From 5a30600c3f05e562b60b682ff3bb26b8fe41a040 Mon Sep 17 00:00:00 2001 From: Sougandh S Date: Fri, 9 Oct 2026 10:39:21 +0530 Subject: [PATCH] Fix broken launch config dialog UI after unsaved-changes prompt on macOS On macOS the tree selects on mouse down, and opening the modal "unsaved changes" prompt at that point corrupts the tree. Skip selection changes while the mouse is pressed and handle them on release. Fixes https://github.com/eclipse-jdt/eclipse.jdt.debug/issues/909 --- .../LaunchConfigurationsDialog.java | 138 +++++++++++++----- 1 file changed, 105 insertions(+), 33 deletions(-) diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java index a5386fae947..d7542db15a8 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java @@ -253,6 +253,23 @@ public static void setCurrentlyVisibleLaunchConfigurationDialog(ILaunchConfigura */ private boolean fSettingInput = false; + /** + * Set while an unsaved-changes prompt triggered by a tree selection change is + * pending or showing + */ + private boolean fHandlingSelectionChange; + + /** + * Whether a mouse button is currently pressed in the launch configuration tree. + */ + private boolean fTreeMouseDown; + + /** + * Work to run as soon as the mouse button is released in the launch + * configuration tree. + */ + private Runnable fRunAfterTreeMouseUp; + /** * Constructs a new launch configuration dialog on the given * parent shell. @@ -608,6 +625,23 @@ else if(status == IDialogConstants.NO_ID) { getLinkPrototypeAction().setConfirmationRequestor(requestor); getUnlinkPrototypeAction().setConfirmationRequestor(requestor); getResetWithPrototypeValuesAction().setConfirmationRequestor(requestor); + viewer.getControl().addListener(SWT.MouseDown, e -> { + // a new press means a pending prompt must not wait for a release we never saw + Runnable pending = fRunAfterTreeMouseUp; + fRunAfterTreeMouseUp = null; + if (pending != null) { + e.display.asyncExec(pending); + } + fTreeMouseDown = true; + }); + viewer.getControl().addListener(SWT.MouseUp, e -> { + fTreeMouseDown = false; + Runnable pending = fRunAfterTreeMouseUp; + fRunAfterTreeMouseUp = null; + if (pending != null) { + e.display.asyncExec(pending); + } + }); ((StructuredViewer) viewer).addPostSelectionChangedListener(event -> { handleLaunchConfigurationSelectionChanged(event); getNewAction().setEnabled(getNewAction().isEnabled()); @@ -1018,12 +1052,13 @@ protected void handleClosePressed() { * @param event selection changed event */ protected void handleLaunchConfigurationSelectionChanged(SelectionChangedEvent event) { + if (fHandlingSelectionChange) { + // a prompt for the previous change is pending/showing, or we are restoring the old selection + return; + } Object input = fTabViewer.getInput(); - Object newInput = null; IStructuredSelection selection = event.getStructuredSelection(); - if (selection.size() == 1) { - newInput = selection.getFirstElement(); - } + Object newInput = selection.size() == 1 ? selection.getFirstElement() : null; if (!isEqual(input, newInput)) { ILaunchConfiguration original = fTabViewer.getOriginal(); if (original != null && newInput == null && getLaunchManager().getMovedTo(original) != null) { @@ -1037,49 +1072,86 @@ protected void handleLaunchConfigurationSelectionChanged(SelectionChangedEvent e if (newInput instanceof ILaunchConfiguration) { renamed = getLaunchManager().getMovedFrom((ILaunchConfiguration)newInput) != null; } - try { - fSettingInput = true; - if (fTabViewer.canSave() && fTabViewer.isDirty() && !deleted && !renamed) { - if(fLaunchConfigurationView != null) { - fLaunchConfigurationView.setAutoSelect(false); - } - int ret = showUnsavedChangesDialog(); - if(ret == IDialogConstants.YES_ID) { - fTabViewer.handleApplyPressed(); - ILaunchConfigurationTab[] tabs = getTabs(); - if (tabs != null) { - for (ILaunchConfigurationTab tab : tabs) { - tab.postApply(); - } + if (fTabViewer.canSave() && fTabViewer.isDirty() && !deleted && !renamed) { + fHandlingSelectionChange = true; + Runnable prompt = () -> { + try { + Shell shell = getShell(); + if (shell != null && !shell.isDisposed()) { + promptAndSwitchInput(input, newInput); } - fTabViewer.setInput(newInput); - } - else if(ret == IDialogConstants.NO_ID) { - fTabViewer.handleRevertPressed(); - fTabViewer.setInput(newInput); - } - else { - fLaunchConfigurationView.getViewer().setSelection(new StructuredSelection(input)); + } finally { + fHandlingSelectionChange = false; } - fLaunchConfigurationView.setAutoSelect(true); + }; + if (fTreeMouseDown) { + fRunAfterTreeMouseUp = prompt; + } else { + prompt.run(); } - else { - fTabViewer.setInput(newInput); - if(fTabViewer.isDirty()) { - fTabViewer.handleApplyPressed(); - } + return; + } + try { + fSettingInput = true; + fTabViewer.setInput(newInput); + if(fTabViewer.isDirty()) { + fTabViewer.handleApplyPressed(); } } finally { fSettingInput = false; updateButtons(); updateMessage(); } - if(getShell() != null && getShell().isVisible()) { + Shell shell = getShell(); + if (shell != null && shell.isVisible()) { resize(); } } } + /** + * Asks how to handle unsaved changes before switching inputs, or restores the + * selection if cancelled. + * + * @param oldInput input currently shown in the tab viewer + * @param newInput newly selected input + */ + private void promptAndSwitchInput(Object oldInput, Object newInput) { + if (fLaunchConfigurationView != null) { + fLaunchConfigurationView.setAutoSelect(false); + } + try { + fSettingInput = true; + int ret = showUnsavedChangesDialog(); + if (ret == IDialogConstants.YES_ID) { + fTabViewer.handleApplyPressed(); + ILaunchConfigurationTab[] tabs = getTabs(); + if (tabs != null) { + for (ILaunchConfigurationTab tab : tabs) { + tab.postApply(); + } + } + fTabViewer.setInput(newInput); + } else if (ret == IDialogConstants.NO_ID) { + fTabViewer.handleRevertPressed(); + fTabViewer.setInput(newInput); + } else if (fLaunchConfigurationView != null && oldInput != null) { + fLaunchConfigurationView.getViewer().setSelection(new StructuredSelection(oldInput), true); + } + } finally { + fSettingInput = false; + if (fLaunchConfigurationView != null) { + fLaunchConfigurationView.setAutoSelect(true); + } + updateButtons(); + updateMessage(); + } + Shell shell = getShell(); + if (shell != null && !shell.isDisposed() && shell.isVisible()) { + resize(); + } + } + /** * Notification the 'launch' button has been pressed. Save and launch. */