From 3dceecd2eec82aa238dd8cc66290b67cf5e10c27 Mon Sep 17 00:00:00 2001 From: Vladlen Shapoval Date: Mon, 5 Oct 2026 21:57:13 +0200 Subject: [PATCH] Added exit confirmation dialogs --- .../core/ui/options/general/Bundle.properties | 3 + .../options/general/GeneralOptionsPanel.form | 10 +++ .../options/general/GeneralOptionsPanel.java | 15 +++- .../src/org/netbeans/core/Bundle.properties | 5 ++ .../src/org/netbeans/core/ExitDialog.java | 73 ++++++++++++++++++- 5 files changed, 103 insertions(+), 3 deletions(-) diff --git a/platform/core.ui/src/org/netbeans/core/ui/options/general/Bundle.properties b/platform/core.ui/src/org/netbeans/core/ui/options/general/Bundle.properties index 6ee765b30a9a..c1b26db18555 100644 --- a/platform/core.ui/src/org/netbeans/core/ui/options/general/Bundle.properties +++ b/platform/core.ui/src/org/netbeans/core/ui/options/general/Bundle.properties @@ -37,6 +37,9 @@ AD_Usage_Statistics=Usage Statistics CTL_Usage_Check=Hel&p us improve the NetBeans IDE by providing anonymous usage data AN_Usage_Check=Help us improve the NetBeans IDE by providing anonymous usage data AD_Usage_Check=Help us improve the NetBeans IDE by providing anonymous usage data +CTL_Exit_Confirmation=&Show confirmation when exiting with no unsaved files +AN_Exit_Confirmation=Show exit confirmation +AD_Exit_Confirmation=Show a confirmation dialog when exiting with no unsaved files CTL_Usage_Info=The usage statistics help us better understand user \ requirements and prioritize improvements in future releases. We will never \ reverse-engineer the collected data to find specific details about your projects. diff --git a/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.form b/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.form index a589573aa14b..86cbb73ecc0d 100644 --- a/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.form +++ b/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.form @@ -103,6 +103,7 @@ + @@ -152,6 +153,8 @@ + + @@ -350,6 +353,13 @@ + + + + + + + diff --git a/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.java b/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.java index 363cc72ba22b..7f89ca3c0f86 100644 --- a/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.java +++ b/platform/core.ui/src/org/netbeans/core/ui/options/general/GeneralOptionsPanel.java @@ -39,6 +39,7 @@ import javax.swing.event.DocumentListener; import org.netbeans.api.options.OptionsDisplayer; import org.netbeans.beaninfo.editors.HtmlBrowser; +import org.netbeans.core.ExitDialog; import org.netbeans.core.ProxySettings; import org.netbeans.spi.options.OptionsPanelController; import org.openide.DialogDescriptor; @@ -127,6 +128,7 @@ public void changedUpdate(DocumentEvent e) { loc (rbNoProxy, "No_Proxy"); loc (rbUseSystemProxy, "Use_System_Proxy_Settings"); loc (rbHTTPProxy, "Use_HTTP_Proxy"); + loc (cbExitConfirmation, "Exit_Confirmation"); loc (lUsage, "Usage_Statistics"); lUsage.getAccessibleContext ().setAccessibleDescription (loc ("AD_Usage_Statistics")); @@ -186,6 +188,7 @@ private void initComponents() { lblLearnMore = new javax.swing.JLabel(); lblUsageInfo = new javax.swing.JLabel(); jUsageCheck = new javax.swing.JCheckBox(); + cbExitConfirmation = new javax.swing.JCheckBox(); lUsage = new javax.swing.JLabel(); bReloadProxy = new javax.swing.JButton(); bTestConnection = new javax.swing.JButton(); @@ -291,6 +294,7 @@ public void mousePressed(java.awt.event.MouseEvent evt) { gridBagConstraints.insets = new java.awt.Insets(0, 15, 5, 0); jPanel1.add(jUsageCheck, gridBagConstraints); + org.openide.awt.Mnemonics.setLocalizedText(cbExitConfirmation, org.openide.util.NbBundle.getMessage(GeneralOptionsPanel.class, "CTL_Exit_Confirmation")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(lUsage, "Usage Statistics:"); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; @@ -373,7 +377,8 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(jSeparator3, javax.swing.GroupLayout.DEFAULT_SIZE, 1495, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) - .addContainerGap()))) + .addContainerGap()) + .addComponent(cbExitConfirmation, javax.swing.GroupLayout.Alignment.LEADING))) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -413,6 +418,8 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(jSeparator3, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cbExitConfirmation) .addContainerGap()) ); @@ -535,6 +542,7 @@ private void bTestConnectionActionPerformed(java.awt.event.ActionEvent evt) {//G private javax.swing.JButton bReloadProxy; private javax.swing.JButton bTestConnection; private javax.swing.JComboBox cbWebBrowser; + private javax.swing.JCheckBox cbExitConfirmation; private javax.swing.JButton editBrowserButton; private javax.swing.JLabel errorLabel; private javax.swing.JPanel jPanel1; @@ -684,6 +692,7 @@ void update () { rbUseSystemProxy.setToolTipText (getUseSystemProxyToolTip ()); jUsageCheck.setSelected(model.getUsageStatistics()); + cbExitConfirmation.setSelected(ExitDialog.isExitConfirmationEnabled()); updateWebBrowsers(); } @@ -761,6 +770,7 @@ void applyChanges () { editor.setAsText ((String) cbWebBrowser.getSelectedItem ()); model.setUsageStatistics(jUsageCheck.isSelected()); + ExitDialog.setExitConfirmationEnabled(cbExitConfirmation.isSelected()); } void cancel () { @@ -774,6 +784,9 @@ boolean isChanged () { if (model == null) { return false; } + if (cbExitConfirmation.isSelected() != ExitDialog.isExitConfirmationEnabled()) { + return true; + } // web browser settings if (editor == null) { editor = Lookup.getDefault().lookup(HtmlBrowser.FactoryEditor.class); diff --git a/platform/o.n.core/src/org/netbeans/core/Bundle.properties b/platform/o.n.core/src/org/netbeans/core/Bundle.properties index 18db39811fee..9eaf96052517 100644 --- a/platform/o.n.core/src/org/netbeans/core/Bundle.properties +++ b/platform/o.n.core/src/org/netbeans/core/Bundle.properties @@ -49,6 +49,11 @@ MSG_AllSaved=Save All finished. CTL_Cannot_save=Cannot save {0} +# Exit confirmation shown when there are no open files with unsaved changes +TTL_ExitConfirmation=Exit +MSG_ExitConfirmation=Do you want to exit? +CTL_ExitConfirmationExit=Exit +CTL_ExitConfirmationCancel=Cancel #NbAuthenticator CTL_PasswordProtected=Password Protected Site diff --git a/platform/o.n.core/src/org/netbeans/core/ExitDialog.java b/platform/o.n.core/src/org/netbeans/core/ExitDialog.java index 3c09c2f0ccde..042d839b1eba 100644 --- a/platform/o.n.core/src/org/netbeans/core/ExitDialog.java +++ b/platform/o.n.core/src/org/netbeans/core/ExitDialog.java @@ -39,6 +39,7 @@ import javax.swing.border.LineBorder; import org.netbeans.api.actions.Savable; import org.openide.DialogDescriptor; +import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.awt.Mnemonics; import org.openide.nodes.Node; @@ -53,6 +54,7 @@ */ public class ExitDialog extends JPanel implements java.awt.event.ActionListener { + private static final String CONFIRM_EXIT_PREFERENCE = "confirmExit"; // NOI18N private static final boolean isAqua = "Aqua".equals(UIManager.getLookAndFeel().getID()); private static Object[] exitOptions; @@ -197,6 +199,16 @@ public static boolean showDialog() { return innerShowDialog(); } + public static boolean isExitConfirmationEnabled() { + return org.openide.util.NbPreferences.forModule(ExitDialog.class) + .getBoolean(CONFIRM_EXIT_PREFERENCE, true); + } + + public static void setExitConfirmationEnabled(boolean enabled) { + org.openide.util.NbPreferences.forModule(ExitDialog.class) + .putBoolean(CONFIRM_EXIT_PREFERENCE, enabled); + } + /** * Opens the ExitDialog. */ @@ -252,8 +264,65 @@ private static boolean innerShowDialog() { return result; } - else - return true; + + else { + if (!isExitConfirmationEnabled()) { + return true; + } + ResourceBundle bundle = NbBundle.getBundle(ExitDialog.class); + + final JButton exitButton = new JButton(); + final JButton cancelButton = new JButton(); + Mnemonics.setLocalizedText(exitButton, bundle.getString("CTL_ExitConfirmationExit")); + Mnemonics.setLocalizedText(cancelButton, bundle.getString("CTL_ExitConfirmationCancel")); + + JPanel buttonRow = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT, 6, 0)); + buttonRow.add(exitButton); + buttonRow.add(cancelButton); + + JLabel message = new JLabel(bundle.getString("MSG_ExitConfirmation"), + UIManager.getIcon("OptionPane.questionIcon"), JLabel.LEADING); + message.setIconTextGap(12); + + JPanel body = new JPanel(new java.awt.BorderLayout(0, 12)); + body.setBorder(BorderFactory.createEmptyBorder(12, 12, 11, 12)); + body.add(message, java.awt.BorderLayout.CENTER); + body.add(buttonRow, java.awt.BorderLayout.SOUTH); + + DialogDescriptor descriptor = new DialogDescriptor( + body, + bundle.getString("TTL_ExitConfirmation"), + true, + new Object[0], // no standard buttons, we have our own + null, + DialogDescriptor.BOTTOM_ALIGN, + null, + (java.awt.event.ActionListener) null); + + final java.awt.Dialog dialog = DialogDisplayer.getDefault().createDialog(descriptor); + dialog.setMinimumSize(new Dimension(225, dialog.getHeight())); + final boolean[] exit = { false }; + exitButton.addActionListener(e -> { exit[0] = true; dialog.setVisible(false); }); + cancelButton.addActionListener(e -> dialog.setVisible(false)); + + javax.swing.JRootPane rootPane = ((javax.swing.JDialog) dialog).getRootPane(); + rootPane.setDefaultButton(exitButton); // Enter = Exit + rootPane.registerKeyboardAction( // Esc = Cancel + e -> dialog.setVisible(false), + javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), + javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW); + + dialog.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowOpened(java.awt.event.WindowEvent e) { + SwingUtilities.invokeLater(exitButton::requestFocusInWindow); + } + }); + dialog.setVisible(true); // blocks (modal) + dialog.dispose(); + + return exit[0]; + } } /** Renderer used in list box of exit dialog