From 5096e6fce3e0719878b10191b815d06eab969f92 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Thu, 25 Jan 2024 15:32:20 +0100 Subject: [PATCH] Expose cancel state of FileDialog and DirectoryDialog The FileDialog and DirectoryDialog implementations do currently not provide any way to identify that the dialog was canceled. The dialogs return a null string in case no file or folder was selected, but is not possible to identify whether this was intended (via a cancel action) or if there was some unexpected error while the dialog was open (such as too long file paths on Windows). This change introduces the `openDialog()` method as an alternative to the existing `open()` method. This new method returns an optional String, which is only present if a file or directory was successfully selected. An empty Optional indicates a cancellation by the user and an actual error is realized as an SWTException. The dialogs evaluate the operating system's response code for the system's dialog control to identify the dialogs result state. --- .../org/eclipse/swt/internal/cocoa/OS.java | 5 +- .../org/eclipse/swt/internal/win32/OS.java | 1 + .../eclipse/swt/widgets/DirectoryDialog.java | 35 ++++++++++++- .../org/eclipse/swt/widgets/FileDialog.java | 33 +++++++++++- .../eclipse/swt/widgets/DirectoryDialog.java | 51 ++++++++++++++++--- .../org/eclipse/swt/widgets/FileDialog.java | 43 ++++++++++++++-- .../eclipse/swt/widgets/DirectoryDialog.java | 41 +++++++++++++-- .../org/eclipse/swt/widgets/FileDialog.java | 35 ++++++++++++- 8 files changed, 223 insertions(+), 21 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java index 873c785a1ed..2f8c5feec27 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java @@ -179,7 +179,7 @@ public static void beginSheetModalForWindow(NSObject id, NSWindow window, long h * Custom message that will be sent when setTheme is called for example from Platform UI code. */ public static final long sel_appAppearanceChanged = OS.sel_registerName("appAppearanceChanged"); - + /** * Experimental API for dark theme. *

@@ -194,7 +194,7 @@ public static void beginSheetModalForWindow(NSObject id, NSWindow window, long h * On GTK, behavior may be different as the boolean flag doesn't force dark * theme instead it specify that dark theme is preferred. *

- * + * * @param isDarkTheme true for dark theme */ public static void setTheme(boolean isDarkTheme) { @@ -2216,6 +2216,7 @@ public static Selector getSelector (long value) { public static final int NSEventTypeMagnify = 30; public static final int NSEventTypeRotate = 18; public static final int NSEventTypeSwipe = 31; +public static final int NSFileHandlingPanelCancelButton = 0; public static final int NSFileHandlingPanelOKButton = 1; public static final int NSFlagsChanged = 12; public static final int NSFocusRingTypeNone = 1; diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java index fd5606d9b4e..c3cfd904f28 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java @@ -452,6 +452,7 @@ public class OS extends C { public static final int EP_EDITTEXT = 1; public static final int ERROR_FILE_NOT_FOUND = 0x2; public static final int ERROR_NO_MORE_ITEMS = 0x103; + public static final int ERROR_CANCELED = 0x4C7; public static final int ESB_DISABLE_BOTH = 0x3; public static final int ESB_ENABLE_BOTH = 0x0; public static final int ES_AUTOHSCROLL = 0x80; diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/DirectoryDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/DirectoryDialog.java index 5098bf8969b..54f6d64a5f9 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/DirectoryDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/DirectoryDialog.java @@ -14,6 +14,8 @@ package org.eclipse.swt.widgets; +import java.util.*; + import org.eclipse.swt.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.internal.cocoa.*; @@ -139,11 +141,16 @@ void handleResponse (long response) { } Display display = parent != null ? parent.getDisplay() : Display.getCurrent(); display.setModalDialog(null); + directoryPath = null; if (response == OS.NSFileHandlingPanelOKButton) { NSString filename = panel.filename(); directoryPath = filterPath = filename.getString(); } releaseHandles(); + + if (response != OS.NSFileHandlingPanelOKButton && response != OS.NSFileHandlingPanelCancelButton) { + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); + } } /** @@ -159,6 +166,32 @@ void handleResponse (long response) { * */ public String open () { + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } +} + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link DirectoryDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the selected directory + * or is empty in case the dialog was canceled + * + * @exception SWTException + * + * @since 3.125 + */ +public Optional openDialog () { directoryPath = null; panel = NSOpenPanel.openPanel(); if (panel == null) { @@ -202,7 +235,7 @@ public String open () { } // options.optionFlags = OS.kNavSupportPackages | OS.kNavAllowOpenPackages | OS.kNavAllowInvisibleFiles; - return directoryPath; + return Optional.ofNullable(directoryPath); } void releaseHandles () { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/FileDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/FileDialog.java index 33f5fc3f2f5..f016b8e8009 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/FileDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/FileDialog.java @@ -294,6 +294,7 @@ void handleResponse (long response) { filterIndex = popup != null ? (int)popup.indexOfSelectedItem() : -1; + fullPath = null; if (response == OS.NSFileHandlingPanelOKButton) { NSString filename = panel.filename(); if ((style & SWT.SAVE) != 0) { @@ -334,6 +335,10 @@ void handleResponse (long response) { } } releaseHandles(); + + if (response != OS.NSFileHandlingPanelOKButton && response != OS.NSFileHandlingPanelCancelButton) { + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); + } } /** @@ -349,6 +354,32 @@ void handleResponse (long response) { * */ public String open () { + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } +} + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link FileDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the first selected file + * or is empty in case the dialog was canceled + * + * @exception SWTException + * + * @since 3.125 + */ +public Optional openDialog () { fullPath = null; if ((style & SWT.SAVE) != 0) { NSSavePanel savePanel = NSSavePanel.savePanel(); @@ -443,7 +474,7 @@ public String open () { long response = panel.runModal(); handleResponse(response); } - return fullPath; + return Optional.ofNullable(fullPath); } long panel_shouldEnableURL (long id, long sel, long arg0, long arg1) { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/DirectoryDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/DirectoryDialog.java index 3e1a39565b1..5eb59380a70 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/DirectoryDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/DirectoryDialog.java @@ -15,6 +15,7 @@ import java.io.*; +import java.util.Optional; import org.eclipse.swt.*; import org.eclipse.swt.internal.*; @@ -122,17 +123,45 @@ public String getMessage () { * */ public String open () { - return openNativeChooserDialog(); + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } } + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link DirectoryDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the selected directory + * or is empty in case the dialog was canceled + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the dialog has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the dialog
  • + *
  • ERROR_INVALID_RETURN_VALUE - if the dialog was not cancelled and did not return a valid path
  • + *
+ * + * @since 3.125 + */ +public Optional openDialog () { + return openNativeChooserDialog(); +} + /** * Open the file chooser dialog using the GtkFileChooserNative API (GTK3.20+) for running applications * without direct filesystem access (such as Flatpak). API for GtkFileChoosernative does not * give access to any GtkWindow or GtkWidget for the dialog, thus this method omits calls that * requires such access. These are be handled by the GtkNativeDialog API. * - * @return a string describing the absolute path of the first selected file, or null + * @return an Optional that contains a string describing the absolute path of the selected directory + * or is empty if the dialog is canceled */ -String openNativeChooserDialog () { +Optional openNativeChooserDialog () { byte [] titleBytes = Converter.wcsToMbcs (title, true); long shellHandle = parent.topHandle (); Display display = parent != null ? parent.getDisplay (): Display.getCurrent (); @@ -168,7 +197,7 @@ String openNativeChooserDialog () { GTK3setNativeDialogMessage(handle, message); - String answer = null; + String selectedPath = null; display.addIdleProc (); int signalId = 0; long hookId = 0; @@ -213,14 +242,22 @@ String openNativeChooserDialog () { char [] chars = new char [clength]; C.memmove (chars, utf16Ptr, clength * 2); OS.g_free (utf16Ptr); - answer = new String (chars); - filterPath = answer; + selectedPath = new String (chars); + filterPath = selectedPath; } } } } + + Optional result = Optional.empty(); + if (response == GTK.GTK_RESPONSE_ACCEPT) { + result = Optional.ofNullable(selectedPath); + } display.removeIdleProc (); - return answer; + if (result.isPresent() || response == GTK.GTK_RESPONSE_CANCEL) { + return result; + } + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/FileDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/FileDialog.java index ef7f487dca3..ffc3e62d19b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/FileDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/FileDialog.java @@ -15,6 +15,7 @@ import java.io.*; +import java.util.Optional; import org.eclipse.swt.*; import org.eclipse.swt.internal.*; @@ -332,17 +333,45 @@ public boolean getOverwrite () { * */ public String open () { + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } +} + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link FileDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the first selected file + * or is empty in case the dialog was canceled + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the dialog has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the dialog
  • + *
  • ERROR_INVALID_RETURN_VALUE - if the dialog was not cancelled and did not return a valid path
  • + *
+ * + * @since 3.125 + */ +public Optional openDialog () { return openNativeChooserDialog(); } + /** * Open the file chooser dialog using the GtkFileChooserNative API (GTK3.20+) for running applications * without direct filesystem access (such as Flatpak). API for GtkFileChoosernative does not * give access to any GtkWindow or GtkWidget for the dialog, thus this method omits calls that * requires such access. These are be handled by the GtkNativeDialog API. * - * @return a string describing the absolute path of the first selected file, or null + * @return an Optional that contains a string describing the absolute path of the first selected file + * or is empty if the dialog is canceled */ -String openNativeChooserDialog () { +Optional openNativeChooserDialog () { byte [] titleBytes = Converter.wcsToMbcs (title, true); int action = (style & SWT.SAVE) != 0 ? GTK.GTK_FILE_CHOOSER_ACTION_SAVE : GTK.GTK_FILE_CHOOSER_ACTION_OPEN; long shellHandle = parent.topHandle(); @@ -356,7 +385,6 @@ String openNativeChooserDialog () { } presetChooserDialog (); display.addIdleProc (); - String answer = null; int signalId = 0; long hookId = 0; if ((style & SWT.RIGHT_TO_LEFT) != 0) { @@ -378,11 +406,16 @@ String openNativeChooserDialog () { if ((style & SWT.RIGHT_TO_LEFT) != 0) { OS.g_signal_remove_emission_hook (signalId, hookId); } + + Optional result = Optional.empty(); if (response == GTK.GTK_RESPONSE_ACCEPT) { - answer = computeResultChooserDialog (); + result = Optional.ofNullable(computeResultChooserDialog ()); } display.removeIdleProc (); - return answer; + if (result.isPresent() || response == GTK.GTK_RESPONSE_CANCEL) { + return result; + } + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); } void presetChooserDialog () { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/DirectoryDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/DirectoryDialog.java index c6fe9f7e327..e521d4f5bc5 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/DirectoryDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/DirectoryDialog.java @@ -15,6 +15,8 @@ package org.eclipse.swt.widgets; +import java.util.*; + import org.eclipse.swt.*; import org.eclipse.swt.internal.ole.win32.*; import org.eclipse.swt.internal.win32.*; @@ -122,10 +124,37 @@ public String getMessage () { *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the dialog
  • * */ -public String open() { +public String open () { + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } +} + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link DirectoryDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the selected directory + * or is empty in case the dialog was canceled + * + * @exception SWTException
      + *
    • ERROR_WIDGET_DISPOSED - if the dialog has been disposed
    • + *
    • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the dialog
    • + *
    • ERROR_INVALID_RETURN_VALUE - if the dialog was not cancelled and did not return a valid path
    • + *
    + * + * @since 3.125 + */ +public Optional openDialog () { this.directoryPath = null; long [] ppv = new long [1]; + int hr = COM.E_FAIL; if (COM.CoCreateInstance(COM.CLSID_FileOpenDialog, 0, COM.CLSCTX_INPROC_SERVER, COM.IID_IFileOpenDialog, ppv) == OS.S_OK) { IFileDialog fileDialog = new IFileDialog(ppv[0]); @@ -162,7 +191,8 @@ public String open() { Display display = parent.getDisplay(); long hwndOwner = parent.handle; display.externalEventLoop = true; - if (fileDialog.Show(hwndOwner) == OS.S_OK) { + hr = fileDialog.Show(hwndOwner); + if (hr == OS.S_OK) { if (fileDialog.GetResult(ppv) == OS.S_OK) { IShellItem psi = new IShellItem(ppv[0]); if (psi.GetDisplayName(OS.SIGDN_FILESYSPATH, ppv) == OS.S_OK) { @@ -182,7 +212,12 @@ public String open() { fileDialog.Release(); } - return directoryPath; + if (hr == COM.S_OK) { + return Optional.ofNullable(directoryPath); + } else if (hr == OS.HRESULT_FROM_WIN32(OS.ERROR_CANCELED)) { + return Optional.empty(); + } + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); } /** diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/FileDialog.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/FileDialog.java index c09ef6e4471..8866382b6f1 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/FileDialog.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/FileDialog.java @@ -14,6 +14,7 @@ package org.eclipse.swt.widgets; import java.nio.file.*; +import java.util.*; import org.eclipse.swt.*; import org.eclipse.swt.internal.ole.win32.*; @@ -216,6 +217,32 @@ static Path getItemPath (IShellItem psi) { * */ public String open () { + try { + return openDialog().orElse(null); + } catch (SWTException e) { + if (e.code == SWT.ERROR_INVALID_RETURN_VALUE) { + return null; + } + throw e; + } +} + +/** + * Makes the dialog visible and brings it to the front of the display. + * Equal to {@link FileDialog#open()} but also exposes for state information like user cancellation. + * + * @return an Optional that either contains the absolute path of the first selected file + * or is empty in case the dialog was canceled + * + * @exception SWTException
      + *
    • ERROR_WIDGET_DISPOSED - if the dialog has been disposed
    • + *
    • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the dialog
    • + *
    • ERROR_INVALID_RETURN_VALUE - if the dialog was not cancelled and did not return a valid path
    • + *
    + * + * @since 3.125 + */ +public Optional openDialog () { /* Create Common Item Dialog */ long[] ppv = new long[1]; int hr; @@ -385,8 +412,12 @@ public String open () { fileDialog.Release(); - /* Answer the full path or null */ - return fullPath; + if (hr == COM.S_OK) { + return Optional.ofNullable(fullPath); + } else if (hr == OS.HRESULT_FROM_WIN32(OS.ERROR_CANCELED)) { + return Optional.empty(); + } + throw new SWTException(SWT.ERROR_INVALID_RETURN_VALUE); } /**