diff --git a/changes/2786.feature.rst b/changes/2786.feature.rst new file mode 100644 index 0000000000..009ef7711a --- /dev/null +++ b/changes/2786.feature.rst @@ -0,0 +1 @@ +Introduced `IFileOpenDialog`, replacing `SHBrowseForFolder` for improved file selection dialogs. diff --git a/testbed/pyproject.toml b/testbed/pyproject.toml index 59dcb37726..695faf4b71 100644 --- a/testbed/pyproject.toml +++ b/testbed/pyproject.toml @@ -66,6 +66,7 @@ test_sources = [ ] requires = [ "../winforms", + "comtypes", ] # Mobile deployments diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index a6a9fc3a14..fef6f569d4 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -1,7 +1,23 @@ import asyncio +import os +from ctypes import ( + HRESULT, + POINTER, + byref, + c_void_p, + c_wchar_p, + cast as cast_with_ctypes, + windll, +) +from ctypes.wintypes import HWND from pathlib import Path +from typing import List, Optional, Tuple, Union +import comtypes +import comtypes.client import System.Windows.Forms as WinForms +from comtypes import GUID +from comtypes.hresult import S_OK from System.Drawing import ( ContentAlignment, Font as WinFont, @@ -11,6 +27,18 @@ ) from System.Windows.Forms import DialogResult, MessageBoxButtons, MessageBoxIcon +from toga_winforms.libs.com.constants import COMDLG_FILTERSPEC, FileOpenOptions +from toga_winforms.libs.com.identifiers import ( + CLSID_FileOpenDialog, + CLSID_FileSaveDialog, +) +from toga_winforms.libs.com.interfaces import ( + IFileOpenDialog, + IFileSaveDialog, + IShellItem, + IShellItemArray, +) + from .libs.wrapper import WeakrefCallable @@ -190,52 +218,85 @@ def winforms_Click_accept(self, sender, event): class FileDialog(BaseDialog): def __init__( self, - native, - title, - initial_directory, + native: Union[IFileOpenDialog, IFileSaveDialog], + title: str, + initial_directory: Union[os.PathLike, str], *, - filename=None, - file_types=None, + filename: Optional[str] = None, + file_types: Optional[List[str]] = None, ): super().__init__() - self.native = native + self.native: Union[IFileOpenDialog, IFileSaveDialog] = native self._set_title(title) if filename is not None: - native.FileName = filename + self.native.SetFileName(filename) if initial_directory is not None: self._set_initial_directory(str(initial_directory)) if file_types is not None: - filters = [f"{ext} files (*.{ext})|*.{ext}" for ext in file_types] + [ - "All files (*.*)|*.*" + filters: List[Tuple[str, str]] = [ + (f"{ext.upper()} files", f"*.{ext}") for ext in file_types ] - - if len(file_types) > 1: - pattern = ";".join([f"*.{ext}" for ext in file_types]) - filters.insert(0, f"All matching files ({pattern})|{pattern}") - - native.Filter = "|".join(filters) + filterspec = (COMDLG_FILTERSPEC * len(file_types))( + *[(c_wchar_p(name), c_wchar_p(spec)) for name, spec in filters] + ) + self.native.SetFileTypes( + len(filterspec), cast_with_ctypes(filterspec, POINTER(c_void_p)) + ) def _show(self): - response = self.native.ShowDialog() - if response == DialogResult.OK: + hwnd = HWND(0) + hr: int = self.native.Show(hwnd) + if hr == S_OK: + assert isinstance( + self, (SaveFileDialog, OpenFileDialog, SelectFolderDialog) + ) self.future.set_result(self._get_filenames()) else: self.future.set_result(None) def _set_title(self, title): - self.native.Title = title + self.native.SetTitle(title) def _set_initial_directory(self, initial_directory): - self.native.InitialDirectory = initial_directory + if initial_directory is None: + return + folder_path: Path = Path(initial_directory).resolve() + if folder_path.is_dir(): # sourcery skip: extract-method + SHCreateItemFromParsingName = windll.shell32.SHCreateItemFromParsingName + SHCreateItemFromParsingName.argtypes = [ + c_wchar_p, # LPCWSTR (wide string, null-terminated) + POINTER( + comtypes.IUnknown + ), # IBindCtx* (can be NULL, hence POINTER(IUnknown)) + POINTER(GUID), # REFIID (pointer to the interface ID, typically GUID) + POINTER( + POINTER(IShellItem) + ), # void** (output pointer to the requested interface) + ] + SHCreateItemFromParsingName.restype = HRESULT + shell_item = POINTER(IShellItem)() + hr = SHCreateItemFromParsingName( + str(folder_path), None, IShellItem._iid_, byref(shell_item) + ) + if hr == S_OK: + self.native.SetFolder(shell_item) class SaveFileDialog(FileDialog): - def __init__(self, title, filename, initial_directory, file_types): + def __init__( + self, + title: str, + filename: str, + initial_directory: Union[os.PathLike, str], + file_types: List[str], + ): super().__init__( - WinForms.SaveFileDialog(), + comtypes.client.CreateObject( + CLSID_FileSaveDialog, interface=IFileSaveDialog + ), title, initial_directory, filename=filename, @@ -243,55 +304,85 @@ def __init__(self, title, filename, initial_directory, file_types): ) def _get_filenames(self): - return Path(self.native.FileName) + shell_item: IShellItem = self.native.GetResult() + display_name: str = shell_item.GetDisplayName(0x80058000) # SIGDN_FILESYSPATH + return Path(display_name) class OpenFileDialog(FileDialog): def __init__( self, - title, - initial_directory, - file_types, - multiple_select, + title: str, + initial_directory: Union[os.PathLike, str], + file_types: List[str], + multiple_select: bool, ): super().__init__( - WinForms.OpenFileDialog(), + comtypes.client.CreateObject( + CLSID_FileOpenDialog, interface=IFileOpenDialog + ), title, initial_directory, file_types=file_types, ) if multiple_select: - self.native.Multiselect = True + self.native.SetOptions(FileOpenOptions.FOS_ALLOWMULTISELECT) - # Provided as a stub that can be mocked in test conditions def selected_paths(self): - return self.native.FileNames - - def _get_filenames(self): - if self.native.Multiselect: - return [Path(filename) for filename in self.selected_paths()] - else: - return Path(self.native.FileName) + # This is a stub method; we provide functionality using the COM API + return self._get_filenames() + + def _get_filenames(self) -> List[Path]: + assert isinstance(self.native, IFileOpenDialog) + results: List[Path] = [] + shell_item_array: IShellItemArray = self.native.GetResults() + item_count: int = shell_item_array.GetCount() + for i in range(item_count): + shell_item: IShellItem = shell_item_array.GetItemAt(i) + szFilePath: str = str( + shell_item.GetDisplayName(0x80058000) + ) # SIGDN_FILESYSPATH + results.append(Path(szFilePath)) + return results class SelectFolderDialog(FileDialog): - def __init__(self, title, initial_directory, multiple_select): + def __init__( + self, + title: str, + initial_directory: Union[os.PathLike, str], + multiple_select: bool, + ): super().__init__( - WinForms.FolderBrowserDialog(), + comtypes.client.CreateObject( + CLSID_FileOpenDialog, + interface=IFileOpenDialog, + ), title, initial_directory, ) + self.native.SetOptions(FileOpenOptions.FOS_PICKFOLDERS) + self.multiple_select: bool = multiple_select - # The native dialog doesn't support multiple selection, so the only effect - # this has is to change whether we return a list. - self.multiple_select = multiple_select - - def _get_filenames(self): - filename = Path(self.native.SelectedPath) - return [filename] if self.multiple_select else filename + def _get_filenames(self) -> Union[List[Path], Path]: + shell_item: IShellItem = self.native.GetResult() + display_name: str = shell_item.GetDisplayName(0x80058000) # SIGDN_FILESYSPATH + return [Path(display_name)] if self.multiple_select else Path(display_name) def _set_title(self, title): - self.native.Description = title + self.native.SetTitle(title) def _set_initial_directory(self, initial_directory): - self.native.SelectedPath = initial_directory + if initial_directory is None: + return + folder_path: Path = Path(initial_directory).resolve() + if folder_path.is_dir(): # sourcery skip: extract-method + shell_item = POINTER(IShellItem)() + hr = windll.shell32.SHCreateItemFromParsingName( + str(folder_path), + None, + IShellItem._iid_, + byref(shell_item), + ) + if hr == S_OK: + self.native.SetFolder(shell_item) diff --git a/winforms/src/toga_winforms/libs/com/constants.py b/winforms/src/toga_winforms/libs/com/constants.py new file mode 100644 index 0000000000..2623140c32 --- /dev/null +++ b/winforms/src/toga_winforms/libs/com/constants.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from ctypes import Structure, c_int, c_ulong +from ctypes.wintypes import LPCWSTR +from enum import IntFlag +from typing import TYPE_CHECKING, Sequence + +if TYPE_CHECKING: + from ctypes import _CData + + +class FileOpenOptions(IntFlag): + FOS_UNKNOWN1 = 0x00000001 + FOS_OVERWRITEPROMPT = 0x00000002 + FOS_STRICTFILETYPES = 0x00000004 + FOS_NOCHANGEDIR = 0x00000008 + FOS_UNKNOWN2 = 0x00000010 + FOS_PICKFOLDERS = 0x00000020 + FOS_FORCEFILESYSTEM = 0x00000040 + FOS_ALLNONSTORAGEITEMS = 0x00000080 + FOS_NOVALIDATE = 0x00000100 + FOS_ALLOWMULTISELECT = 0x00000200 + FOS_UNKNOWN4 = 0x00000400 + FOS_PATHMUSTEXIST = 0x00000800 + FOS_FILEMUSTEXIST = 0x00001000 + FOS_CREATEPROMPT = 0x00002000 + FOS_SHAREAWARE = 0x00004000 + FOS_NOREADONLYRETURN = 0x00008000 + FOS_NOTESTFILECREATE = 0x00010000 + FOS_HIDEMRUPLACES = 0x00020000 + FOS_HIDEPINNEDPLACES = 0x00040000 + FOS_UNKNOWN5 = 0x00080000 + FOS_NODEREFERENCELINKS = 0x00100000 + FOS_UNKNOWN6 = 0x00200000 + FOS_UNKNOWN7 = 0x00400000 + FOS_UNKNOWN8 = 0x00800000 + FOS_UNKNOWN9 = 0x01000000 + FOS_DONTADDTORECENT = 0x02000000 + FOS_UNKNOWN10 = 0x04000000 + FOS_UNKNOWN11 = 0x08000000 + FOS_FORCESHOWHIDDEN = 0x10000000 + FOS_DEFAULTNOMINIMODE = 0x20000000 + FOS_FORCEPREVIEWPANEON = 0x40000000 + FOS_UNKNOWN12 = 0x80000000 + + +# Shell Folder Get Attributes Options +SFGAOF = c_ulong + + +class SFGAO(IntFlag): + SFGAO_CANCOPY = 0x00000001 # Objects can be copied. + SFGAO_CANMOVE = 0x00000002 # Objects can be moved. + SFGAO_CANLINK = 0x00000004 # Objects can be linked. + SFGAO_STORAGE = 0x00000008 # Objects can be stored. + SFGAO_CANRENAME = 0x00000010 # Objects can be renamed. + SFGAO_CANDELETE = 0x00000020 # Objects can be deleted. + SFGAO_HASPROPSHEET = 0x00000040 # Objects have property sheets. + SFGAO_DROPTARGET = 0x00000100 # Objects are drop targets. + SFGAO_CAPABILITYMASK = 0x00000177 # Mask for all capability flags. + SFGAO_ENCRYPTED = 0x00002000 # Object is encrypted (use alt color). + SFGAO_ISSLOW = 0x00004000 # Accessing this object is slow. + SFGAO_GHOSTED = 0x00008000 # Object is ghosted (dimmed). + SFGAO_LINK = 0x00010000 # Shortcut (link). + SFGAO_SHARE = 0x00020000 # Shared. + SFGAO_READONLY = 0x00040000 # Read-only. + SFGAO_HIDDEN = 0x00080000 # Hidden object. + SFGAO_DISPLAYATTRMASK = 0x000FC000 # Mask for display attributes. + SFGAO_FILESYSANCESTOR = 0x10000000 # May contain children with file system folders. + SFGAO_FOLDER = 0x20000000 # Is a folder. + SFGAO_FILESYSTEM = 0x40000000 # Is part of the file system. + SFGAO_HASSUBFOLDER = 0x80000000 # May contain subfolders. + SFGAO_CONTENTSMASK = 0x80000000 # Mask for contents. + SFGAO_VALIDATE = 0x01000000 # Invalidate cached information. + SFGAO_REMOVABLE = 0x02000000 # Is a removable media. + SFGAO_COMPRESSED = 0x04000000 # Object is compressed. + SFGAO_BROWSABLE = 0x08000000 # Supports browsing. + SFGAO_NONENUMERATED = 0x00100000 # Is not enumerated. + SFGAO_NEWCONTENT = 0x00200000 # New content is present. + SFGAO_CANMONIKER = 0x00400000 # Can create monikers for this item. + SFGAO_HASSTORAGE = 0x00400000 # Supports storage interfaces. + SFGAO_STREAM = 0x00400000 # Is a stream object. + SFGAO_STORAGEANCESTOR = 0x00800000 # May contain children with storage folders. + SFGAO_STORAGECAPMASK = 0x70C50008 # Mask for storage capability attributes. + SFGAO_PKEYSFGAOMASK = ( + 0x81044000 # Attributes that are part of the PKEY_SFGAOFlags property. + ) + + +class SIGDN(c_int): + SIGDN_NORMALDISPLAY = 0x00000000 + SIGDN_PARENTRELATIVEPARSING = 0x80018001 + SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8001C001 + SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000 + SIGDN_PARENTRELATIVEEDITING = 0x80031001 + SIGDN_DESKTOPABSOLUTEEDITING = 0x8004C000 + SIGDN_FILESYSPATH = 0x80058000 + SIGDN_URL = 0x80068000 + + +class FDAP(c_int): + FDAP_BOTTOM = 0x00000000 + FDAP_TOP = 0x00000001 + + +class FDE_SHAREVIOLATION_RESPONSE(c_int): # noqa: N801 + FDESVR_DEFAULT = 0x00000000 + FDESVR_ACCEPT = 0x00000001 + FDESVR_REFUSE = 0x00000002 + + +FDE_OVERWRITE_RESPONSE = FDE_SHAREVIOLATION_RESPONSE + + +class COMDLG_FILTERSPEC(Structure): # noqa: N801 + _fields_: Sequence[tuple[str, type[_CData]] | tuple[str, type[_CData], int]] = [ + ("pszName", LPCWSTR), + ("pszSpec", LPCWSTR), + ] diff --git a/winforms/src/toga_winforms/libs/com/identifiers.py b/winforms/src/toga_winforms/libs/com/identifiers.py new file mode 100644 index 0000000000..b6510cc51b --- /dev/null +++ b/winforms/src/toga_winforms/libs/com/identifiers.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from comtypes import GUID + +IID_IUnknown = GUID("{00000000-0000-0000-C000-000000000046}") +IID_IShellItem = GUID("{43826D1E-E718-42EE-BC55-A1E261C37BFE}") +IID_IShellItemArray = GUID("{B63EA76D-1F85-456F-A19C-48159EFA858B}") +IID_IShellItemFilter = GUID("{2659B475-EEB8-48B7-8F07-B378810F48CF}") +IID_IModalWindow = GUID("{B4DB1657-70D7-485E-8E3E-6FCB5A5C1802}") +IID_IFileDialog = GUID("{42F85136-DB7E-439C-85F1-E4075D135FC8}") +IID_IFileSaveDialog = GUID("{84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB}") +IID_IFileOpenDialog = GUID("{D57C7288-D4AD-4768-BE02-9D969532D960}") +IID_IFileDialogEvents = GUID("{973510DB-7D7F-452B-8975-74A85828D354}") + +IID_IFileDialogCustomize = GUID("{E6FDD21A-163F-4975-9C8C-A69F1BA37034}") +IID_IFileDialogControlEvents = GUID("{36116642-D713-4B97-9B83-7484A9D00433}") + +CLSID_FileDialog = GUID("{3D9C8F03-50D4-4E40-BB11-70E74D3F10F3}") +CLSID_FileOpenDialog = GUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}") +CLSID_FileSaveDialog = GUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}") diff --git a/winforms/src/toga_winforms/libs/com/interfaces.py b/winforms/src/toga_winforms/libs/com/interfaces.py new file mode 100644 index 0000000000..7a76105fd0 --- /dev/null +++ b/winforms/src/toga_winforms/libs/com/interfaces.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +from ctypes import HRESULT, POINTER as C_POINTER, c_int, c_uint, c_ulong, c_void_p +from ctypes.wintypes import BOOL, DWORD, HWND, LPCWSTR, LPWSTR, ULONG +from typing import TYPE_CHECKING, Callable, ClassVar + +from comtypes import COMMETHOD, GUID, COMObject, IUnknown + +from toga_winforms.libs.com.identifiers import ( + IID_IFileDialog, + IID_IFileOpenDialog, + IID_IFileSaveDialog, + IID_IModalWindow, + IID_IShellItem, + IID_IShellItemArray, + IID_IShellItemFilter, +) + +if TYPE_CHECKING: + from ctypes import _Pointer + + from comtypes._memberspec import _ComMemberSpec + + +class IModalWindow(IUnknown): + _case_insensitive_: bool = True + _iid_: GUID = IID_IModalWindow + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD([], HRESULT, "Show", (["in"], HWND, "hwndParent")) + ] + Show: Callable[[int | HWND], int] + + +class IShellItem(IUnknown): + _case_insensitive_: bool = True + _iid_: GUID = IID_IShellItem + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD( + [], + HRESULT, + "BindToHandler", + (["in"], C_POINTER(IUnknown), "pbc"), + (["in"], C_POINTER(GUID), "bhid"), + (["in"], C_POINTER(GUID), "riid"), + (["out"], C_POINTER(c_void_p), "ppv"), + ), + COMMETHOD( + [], HRESULT, "GetParent", (["out"], C_POINTER(C_POINTER(IUnknown)), "ppsi") + ), + COMMETHOD( + [], + HRESULT, + "GetDisplayName", + (["in"], c_ulong, "sigdnName"), + (["out"], C_POINTER(LPWSTR), "ppszName"), + ), + COMMETHOD( + [], + HRESULT, + "GetAttributes", + (["in"], c_ulong, "sfgaoMask"), + (["out"], C_POINTER(c_ulong), "psfgaoAttribs"), + ), + COMMETHOD( + [], + HRESULT, + "Compare", + (["in"], C_POINTER(IUnknown), "psi"), + (["in"], c_ulong, "hint"), + (["out"], C_POINTER(c_int), "piOrder"), + ), + ] + QueryInterface: Callable[[GUID, _Pointer[_Pointer[IUnknown]]], int] + AddRef: Callable[[], ULONG] + Release: Callable[[], ULONG] + BindToHandler: Callable[[_Pointer[IUnknown], GUID, GUID, _Pointer[c_void_p]], int] + GetParent: Callable[[], IUnknown] + GetDisplayName: Callable[[c_ulong | int], str] + GetAttributes: Callable[[c_ulong | int], int] + Compare: Callable[[_Pointer[IUnknown], c_ulong, c_int], int] + + +class IShellItemArray(IUnknown): + _case_insensitive_: bool = True + _iid_: GUID = IID_IShellItemArray + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD( + [], + HRESULT, + "BindToHandler", + (["in"], C_POINTER(IUnknown), "pbc"), + (["in"], C_POINTER(GUID), "bhid"), + (["in"], C_POINTER(GUID), "riid"), + (["out"], C_POINTER(c_void_p), "ppv"), + ), + COMMETHOD( + [], + HRESULT, + "GetPropertyStore", + (["in"], c_ulong, "flags"), + (["in"], C_POINTER(GUID), "riid"), + (["out"], C_POINTER(c_void_p), "ppv"), + ), + COMMETHOD( + [], + HRESULT, + "GetPropertyDescriptionList", + (["in"], C_POINTER(GUID), "keyType"), + (["in"], C_POINTER(GUID), "riid"), + (["out"], C_POINTER(c_void_p), "ppv"), + ), + COMMETHOD( + [], + HRESULT, + "GetAttributes", + (["in"], c_ulong, "attribFlags"), + (["in"], c_ulong, "sfgaoMask"), + (["out"], C_POINTER(c_ulong), "psfgaoAttribs"), + ), + COMMETHOD([], HRESULT, "GetCount", (["out"], C_POINTER(c_uint), "pdwNumItems")), + COMMETHOD( + [], + HRESULT, + "GetItemAt", + (["in"], c_uint, "dwIndex"), + (["out"], C_POINTER(C_POINTER(IShellItem)), "ppsi"), + ), + COMMETHOD( + [], + HRESULT, + "EnumItems", + (["out"], C_POINTER(C_POINTER(IUnknown)), "ppenumShellItems"), + ), + ] + QueryInterface: Callable[[GUID, IUnknown], int] + AddRef: Callable[[], ULONG] + Release: Callable[[], ULONG] + BindToHandler: Callable[[_Pointer[IUnknown], GUID, GUID], int] + GetPropertyStore: Callable[[c_ulong, GUID], c_void_p] + GetPropertyDescriptionList: Callable[[GUID, GUID], c_void_p] + GetAttributes: Callable[[c_ulong, c_ulong], _Pointer[c_ulong]] + GetCount: Callable[[], int] + GetItemAt: Callable[[c_uint | int], IShellItem] + EnumItems: Callable[[], IUnknown] + + +class IShellItemFilter(IUnknown): + _case_insensitive_: bool = True + _iid_: GUID = IID_IShellItemFilter + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD([], HRESULT, "IncludeItem", (["in"], C_POINTER(IShellItem), "psi")), + COMMETHOD( + [], + HRESULT, + "GetEnumFlagsForItem", + (["in"], C_POINTER(IShellItem), "psi"), + (["out"], C_POINTER(c_ulong), "pgrfFlags"), + ), + ] + QueryInterface: Callable[[GUID, IUnknown], int] + AddRef: Callable[[], ULONG] + Release: Callable[[], ULONG] + IncludeItem: Callable[[IShellItem], c_ulong] + GetEnumFlagsForItem: Callable[[], int] + + +class IFileDialog(IModalWindow): + _iid_: GUID = IID_IFileDialog + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD( + [], + HRESULT, + "SetFileTypes", + (["in"], c_uint, "cFileTypes"), + (["in"], C_POINTER(c_void_p), "rgFilterSpec"), + ), + COMMETHOD([], HRESULT, "SetFileTypeIndex", (["in"], c_uint, "iFileType")), + COMMETHOD( + [], HRESULT, "GetFileTypeIndex", (["out"], C_POINTER(c_uint), "piFileType") + ), + COMMETHOD( + [], + HRESULT, + "Advise", + (["in"], C_POINTER(IUnknown), "pfde"), + (["out"], C_POINTER(DWORD), "pdwCookie"), + ), + COMMETHOD([], HRESULT, "Unadvise", (["in"], DWORD, "dwCookie")), + COMMETHOD([], HRESULT, "SetOptions", (["in"], c_uint, "fos")), + COMMETHOD([], HRESULT, "GetOptions", (["out"], C_POINTER(DWORD), "pfos")), + COMMETHOD( + [], HRESULT, "SetDefaultFolder", (["in"], C_POINTER(IShellItem), "psi") + ), + COMMETHOD([], HRESULT, "SetFolder", (["in"], C_POINTER(IShellItem), "psi")), + COMMETHOD( + [], + HRESULT, + "GetFolder", + (["out"], C_POINTER(C_POINTER(IShellItem)), "ppsi"), + ), + COMMETHOD( + [], + HRESULT, + "GetCurrentSelection", + (["out"], C_POINTER(C_POINTER(IShellItem)), "ppsi"), + ), + COMMETHOD([], HRESULT, "SetFileName", (["in"], LPCWSTR, "pszName")), + COMMETHOD([], HRESULT, "GetFileName", (["out"], C_POINTER(LPWSTR), "pszName")), + COMMETHOD([], HRESULT, "SetTitle", (["in"], LPCWSTR, "pszTitle")), + COMMETHOD([], HRESULT, "SetOkButtonLabel", (["in"], LPCWSTR, "pszText")), + COMMETHOD([], HRESULT, "SetFileNameLabel", (["in"], LPCWSTR, "pszLabel")), + COMMETHOD( + [], + HRESULT, + "GetResult", + (["out"], C_POINTER(C_POINTER(IShellItem)), "ppsi"), + ), + COMMETHOD( + [], + HRESULT, + "AddPlace", + (["in"], C_POINTER(IShellItem), "psi"), + (["in"], c_int, "fdap"), + ), + COMMETHOD( + [], HRESULT, "SetDefaultExtension", (["in"], LPCWSTR, "pszDefaultExtension") + ), + COMMETHOD([], HRESULT, "Close", (["in"], HRESULT, "hr")), + COMMETHOD([], HRESULT, "SetClientGuid", (["in"], C_POINTER(GUID), "guid")), + COMMETHOD([], HRESULT, "ClearClientData"), + COMMETHOD( + [], HRESULT, "SetFilter", (["in"], C_POINTER(IShellItemFilter), "pFilter") + ), + ] + SetFileTypes: Callable[[c_uint | int, _Pointer[c_void_p]], int] + SetFileTypeIndex: Callable[[c_uint], int] + GetFileTypeIndex: Callable[[], _Pointer[c_uint]] + Advise: Callable[[IUnknown | COMObject], int] + Unadvise: Callable[[int], int] + SetOptions: Callable[[DWORD | int], int] + GetOptions: Callable[[], int] + SetDefaultFolder: Callable[[_Pointer[IShellItem]], int] + SetFolder: Callable[[_Pointer[IShellItem]], int] + GetFolder: Callable[[], IShellItem] + GetCurrentSelection: Callable[[], IShellItem] + SetFileName: Callable[[str], int] + GetFileName: Callable[[], _Pointer[LPWSTR]] + SetTitle: Callable[[str], int] + SetOkButtonLabel: Callable[[str], int] + SetFileNameLabel: Callable[[str], int] + GetResult: Callable[[], IShellItem] + AddPlace: Callable[[IShellItem, c_int], int] + SetDefaultExtension: Callable[[str], int] + Close: Callable[[HRESULT], int] + SetClientGuid: Callable[[GUID], int] + ClearClientData: Callable[[], int] + SetFilter: Callable[[IShellItemFilter], int] + + +class IFileOpenDialog(IFileDialog): + _case_insensitive_: bool = True + _iid_: GUID = IID_IFileOpenDialog + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD( + [], + HRESULT, + "GetResults", + (["out"], C_POINTER(C_POINTER(IShellItemArray)), "ppenum"), + ), + COMMETHOD( + [], + HRESULT, + "GetSelectedItems", + (["out"], C_POINTER(C_POINTER(IShellItemArray)), "ppsai"), + ), + ] + GetResults: Callable[[], IShellItemArray] + GetSelectedItems: Callable[[], IShellItemArray] + + +class IFileSaveDialog(IFileDialog): + _case_insensitive_: bool = True + _iid_: GUID = IID_IFileSaveDialog + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD([], HRESULT, "SetSaveAsItem", (["in"], C_POINTER(IShellItem), "psi")), + COMMETHOD( + [], HRESULT, "SetProperties", (["in"], C_POINTER(IUnknown), "pStore") + ), + COMMETHOD( + [], + HRESULT, + "SetCollectedProperties", + (["in"], C_POINTER(IUnknown), "pList"), + (["in"], BOOL, "fAppendDefault"), + ), + COMMETHOD( + [], + HRESULT, + "GetProperties", + (["out"], C_POINTER(C_POINTER(IUnknown)), "ppStore"), + ), + COMMETHOD( + [], + HRESULT, + "ApplyProperties", + (["in"], C_POINTER(IShellItem), "psi"), + (["in"], C_POINTER(IUnknown), "pStore"), + (["in"], HWND, "hwnd"), + (["in"], C_POINTER(IUnknown), "pSink"), + ), + ] + SetSaveAsItem: Callable[[IShellItem], int] + SetProperties: Callable[[_Pointer[IUnknown]], int] + SetCollectedProperties: Callable[[_Pointer[IUnknown], int], int] + GetProperties: Callable[[_Pointer[_Pointer[IUnknown]]], int] + ApplyProperties: Callable[ + [IShellItem, _Pointer[IUnknown], HWND, _Pointer[IUnknown]], int + ] diff --git a/winforms/src/toga_winforms/libs/com/more_interfaces.py b/winforms/src/toga_winforms/libs/com/more_interfaces.py new file mode 100644 index 0000000000..d24cf7ae25 --- /dev/null +++ b/winforms/src/toga_winforms/libs/com/more_interfaces.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from ctypes import HRESULT, POINTER as C_POINTER, c_bool, c_int, c_uint +from ctypes.wintypes import LPCWSTR, ULONG +from typing import TYPE_CHECKING, Callable, ClassVar + +from comtypes import COMMETHOD, GUID, IUnknown + +from toga_winforms.libs.com.identifiers import ( + IID_IFileDialogControlEvents, + IID_IFileDialogCustomize, + IID_IFileDialogEvents, +) +from toga_winforms.libs.com.interfaces import IFileDialog, IShellItem + +if TYPE_CHECKING: + + from comtypes._memberspec import _ComMemberSpec + + +class IFileDialogEvents(IUnknown): + _case_insensitive_: bool = True + _iid_: GUID = IID_IFileDialogEvents + _methods_: ClassVar[list[_ComMemberSpec]] = [ # noqa: SLF001 + COMMETHOD([], HRESULT, "OnFileOk", (["in"], C_POINTER(IFileDialog), "pfd")), + COMMETHOD( + [], + HRESULT, + "OnFolderChanging", + (["in"], C_POINTER(IFileDialog), "pfd"), + (["in"], C_POINTER(IShellItem), "psiFolder"), + ), + COMMETHOD( + [], HRESULT, "OnFolderChange", (["in"], C_POINTER(IFileDialog), "pfd") + ), + COMMETHOD( + [], HRESULT, "OnSelectionChange", (["in"], C_POINTER(IFileDialog), "pfd") + ), + COMMETHOD( + [], + HRESULT, + "OnShareViolation", + (["in"], C_POINTER(IFileDialog), "pfd"), + (["in"], C_POINTER(IShellItem), "psi"), + (["out"], C_POINTER(c_int), "pResponse"), + ), + COMMETHOD([], HRESULT, "OnTypeChange", (["in"], C_POINTER(IFileDialog), "pfd")), + COMMETHOD( + [], + HRESULT, + "OnOverwrite", + (["in"], C_POINTER(IFileDialog), "pfd"), + (["in"], C_POINTER(IShellItem), "psi"), + (["out"], C_POINTER(c_int), "pResponse"), + ), + ] + QueryInterface: Callable[[GUID, IUnknown], int] + AddRef: Callable[[], ULONG] + Release: Callable[[], ULONG] + OnFileOk: Callable[[IFileDialog], int] + OnFolderChanging: Callable[[IFileDialog, IShellItem], int] + OnFolderChange: Callable[[IFileDialog], int] + OnSelectionChange: Callable[[IFileDialog], int] + OnShareViolation: Callable[[IFileDialog, IShellItem, c_int], int] + OnTypeChange: Callable[[IFileDialog], int] + OnOverwrite: Callable[[IFileDialog, IShellItem, c_int], int] + + +class IFileDialogCustomize(IUnknown): + _case_insensitive_ = True + _iid_: GUID = IID_IFileDialogCustomize + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD([], HRESULT, "EnableOpenDropDown", (["in"], c_uint, "dwIDCtl")), + COMMETHOD( + [], + HRESULT, + "AddText", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszText"), + ), + COMMETHOD( + [], + HRESULT, + "AddPushButton", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD( + [], + HRESULT, + "AddCheckButton", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszLabel"), + (["in"], c_int, "bChecked"), + ), + COMMETHOD([], HRESULT, "AddRadioButtonList", (["in"], c_uint, "dwIDCtl")), + COMMETHOD([], HRESULT, "AddComboBox", (["in"], c_uint, "dwIDCtl")), + COMMETHOD( + [], + HRESULT, + "AddControlItem", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD( + [], + HRESULT, + "AddEditBox", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszText"), + ), + COMMETHOD([], HRESULT, "AddSeparator", (["in"], c_uint, "dwIDCtl")), + COMMETHOD( + [], + HRESULT, + "AddMenu", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD( + [], + HRESULT, + "SetControlLabel", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD( + [], + HRESULT, + "SetControlState", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_int, "dwState"), + ), + COMMETHOD( + [], + HRESULT, + "SetCheckButtonState", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_int, "bChecked"), + ), + COMMETHOD( + [], + HRESULT, + "GetCheckButtonState", + (["in"], c_uint, "dwIDCtl"), + (["out"], C_POINTER(c_int), "pbChecked"), + ), + COMMETHOD( + [], + HRESULT, + "SetEditBoxText", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszText"), + ), + COMMETHOD( + [], + HRESULT, + "GetEditBoxText", + (["in"], c_uint, "dwIDCtl"), + (["out"], C_POINTER(LPCWSTR), "ppszText"), + ), + COMMETHOD( + [], + HRESULT, + "SetControlItemText", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD( + [], + HRESULT, + "GetControlItemState", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + (["out"], C_POINTER(c_int), "pdwState"), + ), + COMMETHOD( + [], + HRESULT, + "SetControlItemState", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + (["in"], c_int, "dwState"), + ), + COMMETHOD( + [], + HRESULT, + "GetSelectedControlItem", + (["in"], c_uint, "dwIDCtl"), + (["out"], C_POINTER(c_uint), "pdwIDItem"), + ), + COMMETHOD( + [], + HRESULT, + "SetSelectedControlItem", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + ), + COMMETHOD( + [], + HRESULT, + "StartVisualGroup", + (["in"], c_uint, "dwIDCtl"), + (["in"], LPCWSTR, "pszLabel"), + ), + COMMETHOD([], HRESULT, "EndVisualGroup", (["in"], c_uint, "dwIDCtl")), + COMMETHOD([], HRESULT, "MakeProminent", (["in"], c_uint, "dwIDCtl")), + COMMETHOD( + [], + HRESULT, + "RemoveControlItem", + (["in"], c_uint, "dwIDCtl"), + (["in"], c_uint, "dwIDItem"), + ), + COMMETHOD([], HRESULT, "RemoveAllControlItems", (["in"], c_uint, "dwIDCtl")), + COMMETHOD( + [], + HRESULT, + "GetControlState", + (["in"], c_uint, "dwIDCtl"), + (["out"], C_POINTER(c_int), "pdwState"), + ), + ] + EnableOpenDropDown: Callable[[int], int] + AddText: Callable[[int, str], int] + AddPushButton: Callable[[int, str], int] + AddCheckButton: Callable[[int, str, int], int] + AddRadioButtonList: Callable[[int], int] + AddComboBox: Callable[[int], int] + AddControlItem: Callable[[int, int, str], int] + AddEditBox: Callable[[int, str], int] + AddSeparator: Callable[[int], int] + AddMenu: Callable[[int, str], int] + SetControlLabel: Callable[[int, str], int] + SetControlState: Callable[[int, int], int] + SetCheckButtonState: Callable[[int, int], int] + GetCheckButtonState: Callable[[int], int] + SetEditBoxText: Callable[[int, str], int] + GetEditBoxText: Callable[[int], LPCWSTR] + SetControlItemText: Callable[[int, int, str], int] + GetControlItemState: Callable[[int, int], int] + SetControlItemState: Callable[[int, int, int], int] + GetSelectedControlItem: Callable[[int], int] + SetSelectedControlItem: Callable[[int, int], int] + StartVisualGroup: Callable[[int, str], int] + EndVisualGroup: Callable[[int], int] + MakeProminent: Callable[[int], int] + RemoveControlItem: Callable[[int, int], int] + RemoveAllControlItems: Callable[[int], int] + GetControlState: Callable[[int], int] + + +class IFileDialogControlEvents(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IFileDialogControlEvents + _methods_: ClassVar[list[_ComMemberSpec]] = [ + COMMETHOD( + [], + HRESULT, + "OnItemSelected", + (["in"], C_POINTER(IFileDialogCustomize), "pfdc"), + (["in"], c_int, "dwIDCtl"), + (["in"], c_int, "dwIDItem"), + ), + COMMETHOD( + [], + HRESULT, + "OnButtonClicked", + (["in"], C_POINTER(IFileDialogCustomize), "pfdc"), + (["in"], c_int, "dwIDCtl"), + ), + COMMETHOD( + [], + HRESULT, + "OnCheckButtonToggled", + (["in"], C_POINTER(IFileDialogCustomize), "pfdc"), + (["in"], c_int, "dwIDCtl"), + (["in"], c_bool, "bChecked"), + ), + COMMETHOD( + [], + HRESULT, + "OnControlActivating", + (["in"], C_POINTER(IFileDialogCustomize), "pfdc"), + (["in"], c_int, "dwIDCtl"), + ), + ] + OnButtonClicked: Callable[[IFileDialogCustomize, c_uint], int] + OnCheckButtonToggled: Callable[[IFileDialogCustomize, c_uint, c_int], int] + OnControlActivating: Callable[[IFileDialogCustomize, c_uint], int] + OnItemSelected: Callable[[IFileDialogCustomize, c_uint, c_uint], int]