From 3f5b6021175dddf5c392571ee27ffeb3a81be269 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 5 Feb 2024 10:20:14 +0000 Subject: [PATCH 1/7] Initialize COM around file dialogs. --- file_windows.go | 50 ++++++++++++++++++++++++++++++++++++------- internal/win/ole32.go | 1 + 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/file_windows.go b/file_windows.go index 15ad749..c29d84b 100644 --- a/file_windows.go +++ b/file_windows.go @@ -1,8 +1,10 @@ package zenity import ( + "context" "fmt" "path/filepath" + "runtime" "syscall" "unicode/utf16" "unsafe" @@ -36,6 +38,12 @@ func selectFile(opts options) (string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) + uninit, err := coInitialize() + if err != nil { + return "", err + } + defer uninit() + defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -79,6 +87,12 @@ func selectFileMultiple(opts options) ([]string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) + uninit, err := coInitialize() + if err != nil { + return nil, err + } + defer uninit() + defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -153,6 +167,12 @@ func selectFileSave(opts options) (string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) + uninit, err := coInitialize() + if err != nil { + return "", err + } + defer uninit() + defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -171,17 +191,15 @@ func selectFileSave(opts options) (string, error) { } func pickFolders(opts options, multi bool) (string, []string, error) { + uninit, err := coInitialize() + if err != nil { + return "", nil, err + } + defer uninit() + owner, _ := opts.attach.(win.HWND) defer setup(owner)() - err := win.CoInitializeEx(0, win.COINIT_APARTMENTTHREADED|win.COINIT_DISABLE_OLE1DDE) - if err != win.RPC_E_CHANGED_MODE { - if err != nil { - return "", nil, err - } - defer win.CoUninitialize() - } - var dialog *win.IFileOpenDialog err = win.CoCreateInstance( win.CLSID_FileOpenDialog, nil, win.CLSCTX_ALL, @@ -320,6 +338,22 @@ func browseForFolderCallback(wnd win.HWND, msg uint32, lparam, data uintptr) uin return 0 } +func coInitialize() (context.CancelFunc, error) { + runtime.LockOSThread() + err := win.CoInitializeEx(0, win.COINIT_APARTMENTTHREADED|win.COINIT_DISABLE_OLE1DDE) + if err == nil || err == win.S_FALSE { + return func() { + win.CoUninitialize() + runtime.UnlockOSThread() + }, nil + } + if err == win.RPC_E_CHANGED_MODE { + return runtime.UnlockOSThread, nil + } + runtime.UnlockOSThread() + return nil, err +} + func initDirNameExt(filename string, name []uint16) (dir *uint16, ext *uint16) { d, n, _ := splitDirAndName(filename) e := filepath.Ext(n) diff --git a/internal/win/ole32.go b/internal/win/ole32.go index be88c70..9b13e36 100644 --- a/internal/win/ole32.go +++ b/internal/win/ole32.go @@ -24,6 +24,7 @@ const ( E_CANCELED = windows.ERROR_CANCELLED | windows.FACILITY_WIN32<<16 | 0x80000000 RPC_E_CHANGED_MODE = syscall.Errno(windows.RPC_E_CHANGED_MODE) + S_FALSE = syscall.Errno(windows.S_FALSE) ) func CoInitializeEx(reserved uintptr, coInit uint32) error { From c56588920f359f4e7a14c7153dd796a7839e3c6b Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 7 Feb 2024 10:50:58 +0000 Subject: [PATCH 2/7] Use IFileOpenDialog where available. --- file_windows.go | 96 ++++++++++++++++++++--------------------- internal/win/shell32.go | 18 ++++++++ 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/file_windows.go b/file_windows.go index c29d84b..c730cfd 100644 --- a/file_windows.go +++ b/file_windows.go @@ -13,9 +13,12 @@ import ( ) func selectFile(opts options) (string, error) { + name, _, shown, err := fileOpenDialog(opts, false) + if shown || opts.ctx != nil && opts.ctx.Err() != nil { + return name, err + } if opts.directory { - res, _, err := pickFolders(opts, false) - return res, err + return browseForFolder(opts) } var args win.OPENFILENAME @@ -38,12 +41,6 @@ func selectFile(opts options) (string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) - uninit, err := coInitialize() - if err != nil { - return "", err - } - defer uninit() - defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -62,9 +59,12 @@ func selectFile(opts options) (string, error) { } func selectFileMultiple(opts options) ([]string, error) { + _, list, shown, err := fileOpenDialog(opts, true) + if shown || opts.ctx != nil && opts.ctx.Err() != nil { + return list, err + } if opts.directory { - _, res, err := pickFolders(opts, true) - return res, err + return nil, fmt.Errorf("%w: multiple directory", ErrUnsupported) } var args win.OPENFILENAME @@ -87,12 +87,6 @@ func selectFileMultiple(opts options) ([]string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) - uninit, err := coInitialize() - if err != nil { - return nil, err - } - defer uninit() - defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -137,8 +131,7 @@ func selectFileMultiple(opts options) ([]string, error) { func selectFileSave(opts options) (string, error) { if opts.directory { - res, _, err := pickFolders(opts, false) - return res, err + return selectFile(opts) } var args win.OPENFILENAME @@ -167,12 +160,6 @@ func selectFileSave(opts options) (string, error) { args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) - uninit, err := coInitialize() - if err != nil { - return "", err - } - defer uninit() - defer setup(args.Owner)() unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { @@ -190,10 +177,10 @@ func selectFileSave(opts options) (string, error) { return syscall.UTF16ToString(res[:]), nil } -func pickFolders(opts options, multi bool) (string, []string, error) { +func fileOpenDialog(opts options, multi bool) (string, []string, bool, error) { uninit, err := coInitialize() if err != nil { - return "", nil, err + return "", nil, false, err } defer uninit() @@ -205,27 +192,27 @@ func pickFolders(opts options, multi bool) (string, []string, error) { win.CLSID_FileOpenDialog, nil, win.CLSCTX_ALL, win.IID_IFileOpenDialog, unsafe.Pointer(&dialog)) if err != nil { - if multi { - return "", nil, fmt.Errorf("%w: multiple directory", ErrUnsupported) - } - return browseForFolder(opts) + return "", nil, false, err } defer dialog.Release() flgs, err := dialog.GetOptions() if err != nil { - return "", nil, err + return "", nil, false, err } - flgs |= win.FOS_NOCHANGEDIR | win.FOS_PICKFOLDERS | win.FOS_FORCEFILESYSTEM + flgs |= win.FOS_NOCHANGEDIR | win.FOS_FILEMUSTEXIST | win.FOS_FORCEFILESYSTEM if multi { flgs |= win.FOS_ALLOWMULTISELECT } + if opts.directory { + flgs |= win.FOS_PICKFOLDERS + } if opts.showHidden { flgs |= win.FOS_FORCESHOWHIDDEN } err = dialog.SetOptions(flgs) if err != nil { - return "", nil, err + return "", nil, false, err } if opts.title != nil { @@ -234,7 +221,12 @@ func pickFolders(opts options, multi bool) (string, []string, error) { if opts.filename != "" { var item *win.IShellItem - win.SHCreateItemFromParsingName(strptr(opts.filename), nil, win.IID_IShellItem, &item) + dir, name, _ := splitDirAndName(opts.filename) + dialog.SetFileName(strptr(name)) + if ext := filepath.Ext(name); len(ext) > 1 { + dialog.SetDefaultExtension(strptr(ext[1:])) + } + win.SHCreateItemFromParsingName(strptr(dir), nil, win.IID_IShellItem, &item) if item != nil { defer item.Release() dialog.SetFolder(item) @@ -243,48 +235,48 @@ func pickFolders(opts options, multi bool) (string, []string, error) { unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { - return "", nil, err + return "", nil, false, err } defer unhook() err = dialog.Show(owner) if opts.ctx != nil && opts.ctx.Err() != nil { - return "", nil, opts.ctx.Err() + return "", nil, true, opts.ctx.Err() } if err == win.E_CANCELED { - return "", nil, ErrCanceled + return "", nil, true, ErrCanceled } if err != nil { - return "", nil, err + return "", nil, true, err } if multi { items, err := dialog.GetResults() if err != nil { - return "", nil, err + return "", nil, true, err } defer items.Release() count, err := items.GetCount() if err != nil { - return "", nil, err + return "", nil, true, err } var lst []string for i := uint32(0); i < count && err == nil; i++ { str, err := shellItemPath(items.GetItemAt(i)) if err != nil { - return "", nil, err + return "", nil, true, err } lst = append(lst, str) } - return "", lst, nil + return "", lst, true, nil } else { str, err := shellItemPath(dialog.GetResult()) if err != nil { - return "", nil, err + return "", nil, true, err } - return str, nil, nil + return str, nil, true, nil } } @@ -296,7 +288,13 @@ func shellItemPath(item *win.IShellItem, err error) (string, error) { return item.GetDisplayName(win.SIGDN_FILESYSPATH) } -func browseForFolder(opts options) (string, []string, error) { +func browseForFolder(opts options) (string, error) { + uninit, err := coInitialize() + if err != nil { + return "", err + } + defer uninit() + var args win.BROWSEINFO args.Owner, _ = opts.attach.(win.HWND) args.Flags = win.BIF_RETURNONLYFSDIRS @@ -311,16 +309,16 @@ func browseForFolder(opts options) (string, []string, error) { unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) if err != nil { - return "", nil, err + return "", err } defer unhook() ptr := win.SHBrowseForFolder(&args) if opts.ctx != nil && opts.ctx.Err() != nil { - return "", nil, opts.ctx.Err() + return "", opts.ctx.Err() } if ptr == nil { - return "", nil, ErrCanceled + return "", ErrCanceled } defer win.CoTaskMemFree(unsafe.Pointer(ptr)) @@ -328,7 +326,7 @@ func browseForFolder(opts options) (string, []string, error) { win.SHGetPathFromIDListEx(ptr, &res[0], len(res), 0) str := syscall.UTF16ToString(res[:]) - return str, []string{str}, nil + return str, nil } func browseForFolderCallback(wnd win.HWND, msg uint32, lparam, data uintptr) uintptr { diff --git a/internal/win/shell32.go b/internal/win/shell32.go index ccfa0f0..04597a3 100644 --- a/internal/win/shell32.go +++ b/internal/win/shell32.go @@ -197,6 +197,15 @@ func (u *IFileDialog) SetFolder(item *IShellItem) (err error) { return } +func (u *IFileDialog) SetFileName(name *uint16) (err error) { + vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) + hr, _, _ := u.call(vtbl.SetFileName, uintptr(unsafe.Pointer(name))) + if hr != 0 { + err = syscall.Errno(hr) + } + return +} + func (u *IFileDialog) SetTitle(title *uint16) (err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.SetTitle, uintptr(unsafe.Pointer(title))) @@ -206,6 +215,15 @@ func (u *IFileDialog) SetTitle(title *uint16) (err error) { return } +func (u *IFileDialog) SetDefaultExtension(extension *uint16) (err error) { + vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) + hr, _, _ := u.call(vtbl.SetDefaultExtension, uintptr(unsafe.Pointer(extension))) + if hr != 0 { + err = syscall.Errno(hr) + } + return +} + func (u *IFileDialog) GetResult() (item *IShellItem, err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.GetResult, uintptr(unsafe.Pointer(&item))) From 77da6e29f388c101bf782c3f67e569a112c51e92 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 7 Feb 2024 13:00:08 +0000 Subject: [PATCH 3/7] Use IFileSaveDialog where available. --- file_windows.go | 84 +++++++++++++++++++++++++++++++++++++++++ internal/win/shell32.go | 12 ++++++ 2 files changed, 96 insertions(+) diff --git a/file_windows.go b/file_windows.go index c730cfd..478b42a 100644 --- a/file_windows.go +++ b/file_windows.go @@ -130,6 +130,10 @@ func selectFileMultiple(opts options) ([]string, error) { } func selectFileSave(opts options) (string, error) { + name, shown, err := fileSaveDialog(opts) + if shown || opts.ctx != nil && opts.ctx.Err() != nil { + return name, err + } if opts.directory { return selectFile(opts) } @@ -280,6 +284,86 @@ func fileOpenDialog(opts options, multi bool) (string, []string, bool, error) { } } +func fileSaveDialog(opts options) (string, bool, error) { + uninit, err := coInitialize() + if err != nil { + return "", false, err + } + defer uninit() + + owner, _ := opts.attach.(win.HWND) + defer setup(owner)() + + var dialog *win.IFileSaveDialog + err = win.CoCreateInstance( + win.CLSID_FileSaveDialog, nil, win.CLSCTX_ALL, + win.IID_IFileSaveDialog, unsafe.Pointer(&dialog)) + if err != nil { + return "", false, err + } + defer dialog.Release() + + flgs, err := dialog.GetOptions() + if err != nil { + return "", false, err + } + flgs |= win.FOS_NOCHANGEDIR | win.FOS_PATHMUSTEXIST | win.FOS_NOREADONLYRETURN | win.FOS_FORCEFILESYSTEM + if opts.confirmOverwrite { + flgs |= win.FOS_OVERWRITEPROMPT + } + if opts.confirmCreate { + flgs |= win.FOS_CREATEPROMPT + } + if opts.showHidden { + flgs |= win.FOS_FORCESHOWHIDDEN + } + err = dialog.SetOptions(flgs) + if err != nil { + return "", false, err + } + + if opts.title != nil { + dialog.SetTitle(strptr(*opts.title)) + } + + if opts.filename != "" { + var item *win.IShellItem + dir, name, _ := splitDirAndName(opts.filename) + dialog.SetFileName(strptr(name)) + if ext := filepath.Ext(name); len(ext) > 1 { + dialog.SetDefaultExtension(strptr(ext[1:])) + } + win.SHCreateItemFromParsingName(strptr(dir), nil, win.IID_IShellItem, &item) + if item != nil { + defer item.Release() + dialog.SetFolder(item) + } + } + + unhook, err := hookDialog(opts.ctx, opts.windowIcon, nil, nil) + if err != nil { + return "", false, err + } + defer unhook() + + err = dialog.Show(owner) + if opts.ctx != nil && opts.ctx.Err() != nil { + return "", true, opts.ctx.Err() + } + if err == win.E_CANCELED { + return "", true, ErrCanceled + } + if err != nil { + return "", true, err + } + + str, err := shellItemPath(dialog.GetResult()) + if err != nil { + return "", true, err + } + return str, true, nil +} + func shellItemPath(item *win.IShellItem, err error) (string, error) { if err != nil { return "", err diff --git a/internal/win/shell32.go b/internal/win/shell32.go index 04597a3..af187d7 100644 --- a/internal/win/shell32.go +++ b/internal/win/shell32.go @@ -123,7 +123,9 @@ type IDLIST struct{} var ( IID_IShellItem = guid("\x1e\x6d\x82\x43\x18\xe7\xee\x42\xbc\x55\xa1\xe2\x61\xc3\x7b\xfe") IID_IFileOpenDialog = guid("\x88\x72\x7c\xd5\xad\xd4\x68\x47\xbe\x02\x9d\x96\x95\x32\xd9\x60") + IID_IFileSaveDialog = guid("\x23\xcd\xbc\x84\xde\x5f\xdb\x4c\xae\xa4\xaf\x64\xb8\x3d\x78\xab") CLSID_FileOpenDialog = guid("\x9c\x5a\x1c\xdc\x8a\xe8\xde\x4d\xa5\xa1\x60\xf8\x2a\x20\xae\xf7") + CLSID_FileSaveDialog = guid("\xf3\xe2\xb4\xc0\x21\xba\x73\x47\x8d\xba\x33\x5e\xc9\x46\xeb\x8b") ) type IFileOpenDialog struct{ IFileDialog } @@ -142,6 +144,16 @@ func (u *IFileOpenDialog) GetResults() (res *IShellItemArray, err error) { return } +type IFileSaveDialog struct{ IFileDialog } +type IFileSaveDialogVtbl struct { + iFileDialogVtbl + SetSaveAsItem uintptr + SetProperties uintptr + SetCollectedProperties uintptr + GetProperties uintptr + ApplyProperties uintptr +} + type IFileDialog struct{ IModalWindow } type iFileDialogVtbl struct { iModalWindowVtbl From 903df781a8d0ff49ee6e25491267d3107c6b9f2b Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 7 Feb 2024 18:58:08 +0000 Subject: [PATCH 4/7] SetFileTypes. --- file_test.go | 17 +++-- file_windows.go | 34 ++++++++-- internal/win/shell32.go | 106 +++++++++++++++++++------------ internal/win/zsyscall_windows.go | 6 +- 4 files changed, 106 insertions(+), 57 deletions(-) diff --git a/file_test.go b/file_test.go index 8cafe97..0fe0d8e 100644 --- a/file_test.go +++ b/file_test.go @@ -3,7 +3,6 @@ package zenity_test import ( "context" "errors" - "fmt" "os" "path/filepath" "testing" @@ -124,7 +123,7 @@ func TestSelectFile_script(t *testing.T) { } t.Run("Cancel", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, cancel.")) + zenity.Info("In the file selection dialog, cancel.") str, err := zenity.SelectFile() if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -134,7 +133,7 @@ func TestSelectFile_script(t *testing.T) { } }) t.Run("File", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, pick any file.")) + zenity.Info("In the file selection dialog, pick any file.") str, err := zenity.SelectFile() if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -147,7 +146,7 @@ func TestSelectFile_script(t *testing.T) { } }) t.Run("Directory", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, pick any directory.")) + zenity.Info("In the file selection dialog, pick any directory.") str, err := zenity.SelectFile(zenity.Directory()) if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -169,7 +168,7 @@ func TestSelectFileMultiple_script(t *testing.T) { } t.Run("Cancel", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, cancel.")) + zenity.Info("In the file selection dialog, cancel.") lst, err := zenity.SelectFileMultiple() if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -179,7 +178,7 @@ func TestSelectFileMultiple_script(t *testing.T) { } }) t.Run("Files", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, pick two files.")) + zenity.Info("In the file selection dialog, pick two files.") lst, err := zenity.SelectFileMultiple() if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -194,7 +193,7 @@ func TestSelectFileMultiple_script(t *testing.T) { } }) t.Run("Directories", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file selection dialog, pick two directories.")) + zenity.Info("In the file selection dialog, pick two directories.") lst, err := zenity.SelectFileMultiple(zenity.Directory()) if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -221,7 +220,7 @@ func TestSelectFileSave_script(t *testing.T) { } t.Run("Cancel", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file save dialog, cancel.")) + zenity.Info("In the file save dialog, cancel.") str, err := zenity.SelectFileSave() if skip, err := skip(err); skip { t.Skip("skipping:", err) @@ -231,7 +230,7 @@ func TestSelectFileSave_script(t *testing.T) { } }) t.Run("Name", func(t *testing.T) { - zenity.Info(fmt.Sprintf("In the file save dialog, press OK.")) + zenity.Info("In the file save dialog, press OK.") str, err := zenity.SelectFileSave( zenity.ConfirmOverwrite(), zenity.Filename("Χρτο.go"), diff --git a/file_windows.go b/file_windows.go index 478b42a..5b3a9c5 100644 --- a/file_windows.go +++ b/file_windows.go @@ -222,6 +222,9 @@ func fileOpenDialog(opts options, multi bool) (string, []string, bool, error) { if opts.title != nil { dialog.SetTitle(strptr(*opts.title)) } + if opts.fileFilters != nil { + dialog.SetFileTypes(initFileTypes(opts.fileFilters)) + } if opts.filename != "" { var item *win.IShellItem @@ -325,6 +328,9 @@ func fileSaveDialog(opts options) (string, bool, error) { if opts.title != nil { dialog.SetTitle(strptr(*opts.title)) } + if opts.fileFilters != nil { + dialog.SetFileTypes(initFileTypes(opts.fileFilters)) + } if opts.filename != "" { var item *win.IShellItem @@ -459,11 +465,10 @@ func initFilters(filters FileFilters) *uint16 { if len(f.Patterns) == 0 { continue } - res = append(res, utf16.Encode([]rune(f.Name))...) - res = append(res, 0) + res = append(res, syscall.StringToUTF16(f.Name)...) for _, p := range f.Patterns { - res = append(res, utf16.Encode([]rune(p))...) - res = append(res, uint16(';')) + res = append(res, syscall.StringToUTF16(p)...) + res[len(res)-1] = ';' } res = append(res, 0) } @@ -473,3 +478,24 @@ func initFilters(filters FileFilters) *uint16 { } return nil } + +func initFileTypes(filters FileFilters) (int, *win.COMDLG_FILTERSPEC) { + filters.simplify() + filters.name() + var res []win.COMDLG_FILTERSPEC + for _, f := range filters { + if len(f.Patterns) == 0 { + continue + } + var spec []uint16 + for _, p := range f.Patterns { + spec = append(spec, syscall.StringToUTF16(p)...) + spec[len(spec)-1] = ';' + } + res = append(res, win.COMDLG_FILTERSPEC{ + Name: syscall.StringToUTF16Ptr(f.Name), + Spec: &spec[0], + }) + } + return len(res), &res[0] +} diff --git a/internal/win/shell32.go b/internal/win/shell32.go index af187d7..5b107e4 100644 --- a/internal/win/shell32.go +++ b/internal/win/shell32.go @@ -46,43 +46,51 @@ const ( // NOTIFYICONDATA state NIS_HIDDEN = 0x1 NIS_SHAREDICON = 0x2 +) - // IFileOpenDialog options - FOS_OVERWRITEPROMPT = 0x2 - FOS_STRICTFILETYPES = 0x4 - FOS_NOCHANGEDIR = 0x8 - FOS_PICKFOLDERS = 0x20 - FOS_FORCEFILESYSTEM = 0x40 - FOS_ALLNONSTORAGEITEMS = 0x80 - FOS_NOVALIDATE = 0x100 - FOS_ALLOWMULTISELECT = 0x200 - FOS_PATHMUSTEXIST = 0x800 - FOS_FILEMUSTEXIST = 0x1000 - FOS_CREATEPROMPT = 0x2000 - FOS_SHAREAWARE = 0x4000 - FOS_NOREADONLYRETURN = 0x8000 - FOS_NOTESTFILECREATE = 0x10000 - FOS_HIDEMRUPLACES = 0x20000 - FOS_HIDEPINNEDPLACES = 0x40000 - FOS_NODEREFERENCELINKS = 0x100000 - FOS_OKBUTTONNEEDSINTERACTION = 0x200000 - FOS_DONTADDTORECENT = 0x2000000 - FOS_FORCESHOWHIDDEN = 0x10000000 - FOS_DEFAULTNOMINIMODE = 0x20000000 - FOS_FORCEPREVIEWPANEON = 0x40000000 - FOS_SUPPORTSTREAMABLEITEMS = 0x80000000 +// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions +type _FILEOPENDIALOGOPTIONS int - // IShellItem.GetDisplayName forms - SIGDN_NORMALDISPLAY = 0x00000000 - SIGDN_PARENTRELATIVEPARSING = ^(^0x18001 + 0x80000000) - SIGDN_DESKTOPABSOLUTEPARSING = ^(^0x28000 + 0x80000000) - SIGDN_PARENTRELATIVEEDITING = ^(^0x31001 + 0x80000000) - SIGDN_DESKTOPABSOLUTEEDITING = ^(^0x4c000 + 0x80000000) - SIGDN_FILESYSPATH = ^(^0x58000 + 0x80000000) - SIGDN_URL = ^(^0x68000 + 0x80000000) - SIGDN_PARENTRELATIVEFORADDRESSBAR = ^(^0x7c001 + 0x80000000) - SIGDN_PARENTRELATIVE = ^(^0x80001 + 0x80000000) - SIGDN_PARENTRELATIVEFORUI = ^(^0x94001 + 0x80000000) +const ( + FOS_OVERWRITEPROMPT _FILEOPENDIALOGOPTIONS = 0x2 + FOS_STRICTFILETYPES _FILEOPENDIALOGOPTIONS = 0x4 + FOS_NOCHANGEDIR _FILEOPENDIALOGOPTIONS = 0x8 + FOS_PICKFOLDERS _FILEOPENDIALOGOPTIONS = 0x20 + FOS_FORCEFILESYSTEM _FILEOPENDIALOGOPTIONS = 0x40 + FOS_ALLNONSTORAGEITEMS _FILEOPENDIALOGOPTIONS = 0x80 + FOS_NOVALIDATE _FILEOPENDIALOGOPTIONS = 0x100 + FOS_ALLOWMULTISELECT _FILEOPENDIALOGOPTIONS = 0x200 + FOS_PATHMUSTEXIST _FILEOPENDIALOGOPTIONS = 0x800 + FOS_FILEMUSTEXIST _FILEOPENDIALOGOPTIONS = 0x1000 + FOS_CREATEPROMPT _FILEOPENDIALOGOPTIONS = 0x2000 + FOS_SHAREAWARE _FILEOPENDIALOGOPTIONS = 0x4000 + FOS_NOREADONLYRETURN _FILEOPENDIALOGOPTIONS = 0x8000 + FOS_NOTESTFILECREATE _FILEOPENDIALOGOPTIONS = 0x10000 + FOS_HIDEMRUPLACES _FILEOPENDIALOGOPTIONS = 0x20000 + FOS_HIDEPINNEDPLACES _FILEOPENDIALOGOPTIONS = 0x40000 + FOS_NODEREFERENCELINKS _FILEOPENDIALOGOPTIONS = 0x100000 + FOS_OKBUTTONNEEDSINTERACTION _FILEOPENDIALOGOPTIONS = 0x200000 + FOS_DONTADDTORECENT _FILEOPENDIALOGOPTIONS = 0x2000000 + FOS_FORCESHOWHIDDEN _FILEOPENDIALOGOPTIONS = 0x10000000 + FOS_DEFAULTNOMINIMODE _FILEOPENDIALOGOPTIONS = 0x20000000 + FOS_FORCEPREVIEWPANEON _FILEOPENDIALOGOPTIONS = 0x40000000 + FOS_SUPPORTSTREAMABLEITEMS _FILEOPENDIALOGOPTIONS = 0x80000000 +) + +// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-sigdn +type SIGDN int + +const ( + SIGDN_NORMALDISPLAY SIGDN = 0x00000000 + SIGDN_PARENTRELATIVEPARSING SIGDN = ^(^0x18001 + 0x80000000) + SIGDN_DESKTOPABSOLUTEPARSING SIGDN = ^(^0x28000 + 0x80000000) + SIGDN_PARENTRELATIVEEDITING SIGDN = ^(^0x31001 + 0x80000000) + SIGDN_DESKTOPABSOLUTEEDITING SIGDN = ^(^0x4c000 + 0x80000000) + SIGDN_FILESYSPATH SIGDN = ^(^0x58000 + 0x80000000) + SIGDN_URL SIGDN = ^(^0x68000 + 0x80000000) + SIGDN_PARENTRELATIVEFORADDRESSBAR SIGDN = ^(^0x7c001 + 0x80000000) + SIGDN_PARENTRELATIVE SIGDN = ^(^0x80001 + 0x80000000) + SIGDN_PARENTRELATIVEFORUI SIGDN = ^(^0x94001 + 0x80000000) ) // https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfow @@ -116,7 +124,14 @@ type NOTIFYICONDATA struct { // BalloonIcon Handle // NOTIFYICONDATAA_V3_SIZE } -type IDLIST struct{} +// https://docs.microsoft.com/en-us/windows/win32/api/shtypes/ns-shtypes-comdlg_filterspec +type COMDLG_FILTERSPEC struct { + Name *uint16 + Spec *uint16 +} + +// https://docs.microsoft.com/en-us/windows/win32/api/shtypes/ns-shtypes-itemidlist +type ITEMIDLIST struct{} // https://github.com/wine-mirror/wine/blob/master/include/shobjidl.idl @@ -182,7 +197,16 @@ type iFileDialogVtbl struct { SetFilter uintptr } -func (u *IFileDialog) SetOptions(fos int) (err error) { +func (u *IFileDialog) SetFileTypes(cFileTypes int, rgFilterSpec *COMDLG_FILTERSPEC) (err error) { + vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) + hr, _, _ := u.call(vtbl.SetFileTypes, uintptr(cFileTypes), uintptr(unsafe.Pointer(rgFilterSpec))) + if hr != 0 { + err = syscall.Errno(hr) + } + return +} + +func (u *IFileDialog) SetOptions(fos _FILEOPENDIALOGOPTIONS) (err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.SetOptions, uintptr(fos)) if hr != 0 { @@ -191,7 +215,7 @@ func (u *IFileDialog) SetOptions(fos int) (err error) { return } -func (u *IFileDialog) GetOptions() (fos int, err error) { +func (u *IFileDialog) GetOptions() (fos _FILEOPENDIALOGOPTIONS, err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.GetOptions, uintptr(unsafe.Pointer(&fos))) if hr != 0 { @@ -270,7 +294,7 @@ type iShellItemVtbl struct { Compare uintptr } -func (u *IShellItem) GetDisplayName(name int) (res string, err error) { +func (u *IShellItem) GetDisplayName(name SIGDN) (res string, err error) { var ptr *uint16 vtbl := *(**iShellItemVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.GetDisplayName, uintptr(name), uintptr(unsafe.Pointer(&ptr))) @@ -314,7 +338,7 @@ func (u *IShellItemArray) GetItemAt(index uint32) (item *IShellItem, err error) } //sys ExtractAssociatedIcon(instance Handle, path *uint16, icon *uint16) (ret Handle, err error) = shell32.ExtractAssociatedIconW -//sys SHBrowseForFolder(bi *BROWSEINFO) (ret *IDLIST) = shell32.SHBrowseForFolder +//sys SHBrowseForFolder(bi *BROWSEINFO) (ret *ITEMIDLIST) = shell32.SHBrowseForFolder //sys SHCreateItemFromParsingName(path *uint16, bc *IBindCtx, iid *GUID, item **IShellItem) (res error) = shell32.SHCreateItemFromParsingName //sys ShellNotifyIcon(message uint32, data *NOTIFYICONDATA) (ok bool) = shell32.Shell_NotifyIconW -//sys SHGetPathFromIDListEx(ptr *IDLIST, path *uint16, pathLen int, opts int) (ok bool) = shell32.SHGetPathFromIDListEx +//sys SHGetPathFromIDListEx(ptr *ITEMIDLIST, path *uint16, pathLen int, opts int) (ok bool) = shell32.SHGetPathFromIDListEx diff --git a/internal/win/zsyscall_windows.go b/internal/win/zsyscall_windows.go index d5f6d8e..0db7013 100644 --- a/internal/win/zsyscall_windows.go +++ b/internal/win/zsyscall_windows.go @@ -235,9 +235,9 @@ func ExtractAssociatedIcon(instance Handle, path *uint16, icon *uint16) (ret Han return } -func SHBrowseForFolder(bi *BROWSEINFO) (ret *IDLIST) { +func SHBrowseForFolder(bi *BROWSEINFO) (ret *ITEMIDLIST) { r0, _, _ := syscall.Syscall(procSHBrowseForFolder.Addr(), 1, uintptr(unsafe.Pointer(bi)), 0, 0) - ret = (*IDLIST)(unsafe.Pointer(r0)) + ret = (*ITEMIDLIST)(unsafe.Pointer(r0)) return } @@ -249,7 +249,7 @@ func SHCreateItemFromParsingName(path *uint16, bc *IBindCtx, iid *GUID, item **I return } -func SHGetPathFromIDListEx(ptr *IDLIST, path *uint16, pathLen int, opts int) (ok bool) { +func SHGetPathFromIDListEx(ptr *ITEMIDLIST, path *uint16, pathLen int, opts int) (ok bool) { r0, _, _ := syscall.Syscall6(procSHGetPathFromIDListEx.Addr(), 4, uintptr(unsafe.Pointer(ptr)), uintptr(unsafe.Pointer(path)), uintptr(pathLen), uintptr(opts), 0, 0) ok = r0 != 0 return From 1e77b878b58423c6c81f6a09c2efaeab6e1de632 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 7 Feb 2024 19:05:07 +0000 Subject: [PATCH 5/7] Reorder. --- file_windows.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/file_windows.go b/file_windows.go index 5b3a9c5..bff5d6c 100644 --- a/file_windows.go +++ b/file_windows.go @@ -130,13 +130,13 @@ func selectFileMultiple(opts options) ([]string, error) { } func selectFileSave(opts options) (string, error) { + if opts.directory { + return selectFile(opts) + } name, shown, err := fileSaveDialog(opts) if shown || opts.ctx != nil && opts.ctx.Err() != nil { return name, err } - if opts.directory { - return selectFile(opts) - } var args win.OPENFILENAME args.StructSize = uint32(unsafe.Sizeof(args)) From 99f1a258bcfc7eaa8b3a90332e51ae06f8768454 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 23 Feb 2024 13:06:35 +0000 Subject: [PATCH 6/7] Don't CoUninitialize, close MTA dialog. --- file_windows.go | 43 ++++++++++++++++++++++++++++++++++------- internal/win/ole32.go | 1 + internal/win/shell32.go | 13 +++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/file_windows.go b/file_windows.go index bff5d6c..a117009 100644 --- a/file_windows.go +++ b/file_windows.go @@ -246,6 +246,18 @@ func fileOpenDialog(opts options, multi bool) (string, []string, bool, error) { } defer unhook() + if opts.ctx != nil && opts.ctx.Done() != nil { + wait := make(chan struct{}) + defer close(wait) + go func() { + select { + case <-opts.ctx.Done(): + dialog.Close(win.E_TIMEOUT) + case <-wait: + } + }() + } + err = dialog.Show(owner) if opts.ctx != nil && opts.ctx.Err() != nil { return "", nil, true, opts.ctx.Err() @@ -352,6 +364,18 @@ func fileSaveDialog(opts options) (string, bool, error) { } defer unhook() + if opts.ctx != nil && opts.ctx.Done() != nil { + wait := make(chan struct{}) + defer close(wait) + go func() { + select { + case <-opts.ctx.Done(): + dialog.Close(win.E_TIMEOUT) + case <-wait: + } + }() + } + err = dialog.Show(owner) if opts.ctx != nil && opts.ctx.Err() != nil { return "", true, opts.ctx.Err() @@ -428,14 +452,19 @@ func browseForFolderCallback(wnd win.HWND, msg uint32, lparam, data uintptr) uin func coInitialize() (context.CancelFunc, error) { runtime.LockOSThread() - err := win.CoInitializeEx(0, win.COINIT_APARTMENTTHREADED|win.COINIT_DISABLE_OLE1DDE) - if err == nil || err == win.S_FALSE { - return func() { - win.CoUninitialize() - runtime.UnlockOSThread() - }, nil + // .NET uses MTA for all background threads, so do the same. + // If someone needs STA because they're doing UI, + // they should initialize COM themselves before. + err := win.CoInitializeEx(0, win.COINIT_MULTITHREADED|win.COINIT_DISABLE_OLE1DDE) + if err == win.S_FALSE { + // COM was already initialized, we simply increased the ref count. + // Make this a no-op by decreasing our ref count. + win.CoUninitialize() + return runtime.UnlockOSThread, nil } - if err == win.RPC_E_CHANGED_MODE { + // Don't uninitialize COM; this is against the docs, but it's what .NET does. + // Eventually all threads will have COM initialized. + if err == nil || err == win.RPC_E_CHANGED_MODE { return runtime.UnlockOSThread, nil } runtime.UnlockOSThread() diff --git a/internal/win/ole32.go b/internal/win/ole32.go index 9b13e36..9383a93 100644 --- a/internal/win/ole32.go +++ b/internal/win/ole32.go @@ -23,6 +23,7 @@ const ( CLSCTX_ALL = windows.CLSCTX_INPROC_SERVER | windows.CLSCTX_INPROC_HANDLER | windows.CLSCTX_LOCAL_SERVER | windows.CLSCTX_REMOTE_SERVER E_CANCELED = windows.ERROR_CANCELLED | windows.FACILITY_WIN32<<16 | 0x80000000 + E_TIMEOUT = windows.ERROR_TIMEOUT | windows.FACILITY_WIN32<<16 | 0x80000000 RPC_E_CHANGED_MODE = syscall.Errno(windows.RPC_E_CHANGED_MODE) S_FALSE = syscall.Errno(windows.S_FALSE) ) diff --git a/internal/win/shell32.go b/internal/win/shell32.go index 5b107e4..61cb0ab 100644 --- a/internal/win/shell32.go +++ b/internal/win/shell32.go @@ -251,6 +251,15 @@ func (u *IFileDialog) SetTitle(title *uint16) (err error) { return } +func (u *IFileDialog) GetResult() (item *IShellItem, err error) { + vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) + hr, _, _ := u.call(vtbl.GetResult, uintptr(unsafe.Pointer(&item))) + if hr != 0 { + err = syscall.Errno(hr) + } + return +} + func (u *IFileDialog) SetDefaultExtension(extension *uint16) (err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) hr, _, _ := u.call(vtbl.SetDefaultExtension, uintptr(unsafe.Pointer(extension))) @@ -260,9 +269,9 @@ func (u *IFileDialog) SetDefaultExtension(extension *uint16) (err error) { return } -func (u *IFileDialog) GetResult() (item *IShellItem, err error) { +func (u *IFileDialog) Close(res syscall.Errno) (err error) { vtbl := *(**iFileDialogVtbl)(unsafe.Pointer(u)) - hr, _, _ := u.call(vtbl.GetResult, uintptr(unsafe.Pointer(&item))) + hr, _, _ := u.call(vtbl.Close, uintptr(res)) if hr != 0 { err = syscall.Errno(hr) } From 649da2441de431b12e2deee9140eb89c03c98c5f Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 23 Feb 2024 13:07:06 +0000 Subject: [PATCH 7/7] Optimize contexts that never cancel. --- date_windows.go | 2 +- entry_windows.go | 2 +- list_windows.go | 2 +- notify_windows.go | 2 +- progress_windows.go | 2 +- pwd_windows.go | 2 +- util_windows.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/date_windows.go b/date_windows.go index 40fdf06..33b84a6 100644 --- a/date_windows.go +++ b/date_windows.go @@ -102,7 +102,7 @@ func (dlg *calendarDialog) setup(text string, opts options) (time.Time, error) { win.SetFocus(dlg.dateCtl) win.ShowWindow(dlg.wnd, win.SW_NORMAL) - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { wait := make(chan struct{}) defer close(wait) go func() { diff --git a/entry_windows.go b/entry_windows.go index 6b10dc2..ee81193 100644 --- a/entry_windows.go +++ b/entry_windows.go @@ -97,7 +97,7 @@ func (dlg *entryDialog) setup(text string, opts options) (string, error) { win.ShowWindow(dlg.wnd, win.SW_NORMAL) win.SendMessage(dlg.editCtl, win.EM_SETSEL, 0, intptr(-1)) - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { wait := make(chan struct{}) defer close(wait) go func() { diff --git a/list_windows.go b/list_windows.go index 605b426..70fcad3 100644 --- a/list_windows.go +++ b/list_windows.go @@ -128,7 +128,7 @@ func (dlg *listDialog) setup(text string, opts options) ([]string, error) { win.SetFocus(dlg.listCtl) win.ShowWindow(dlg.wnd, win.SW_NORMAL) - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { wait := make(chan struct{}) defer close(wait) go func() { diff --git a/notify_windows.go b/notify_windows.go index 39e40f8..808f20b 100644 --- a/notify_windows.go +++ b/notify_windows.go @@ -121,7 +121,7 @@ func shellNotify(text string, opts options) error { major, minor, _ := win.RtlGetNtVersionNumbers() // On Windows 7 (6.1) and lower, wait up to 10 seconds to clean up. if major < 6 || major == 6 && minor < 2 { - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { select { case <-opts.ctx.Done(): case <-time.After(10 * time.Second): diff --git a/progress_windows.go b/progress_windows.go index c9d3fa1..e67dea6 100644 --- a/progress_windows.go +++ b/progress_windows.go @@ -190,7 +190,7 @@ func (dlg *progressDialog) setup(opts options) error { } once.Do(dlg.init.Done) - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { wait := make(chan struct{}) defer close(wait) go func() { diff --git a/pwd_windows.go b/pwd_windows.go index 58b7d6c..d4fe3c2 100644 --- a/pwd_windows.go +++ b/pwd_windows.go @@ -113,7 +113,7 @@ func (dlg *passwordDialog) setup(opts options) (string, string, error) { win.ShowWindow(dlg.wnd, win.SW_NORMAL) win.SendMessage(dlg.uEditCtl, win.EM_SETSEL, 0, intptr(-1)) - if opts.ctx != nil { + if opts.ctx != nil && opts.ctx.Done() != nil { wait := make(chan struct{}) defer close(wait) go func() { diff --git a/util_windows.go b/util_windows.go index 7f609af..8ee8870 100644 --- a/util_windows.go +++ b/util_windows.go @@ -118,7 +118,7 @@ func newDialogHook(ctx context.Context, icon any, title *string, init func(wnd w title: title, init: init, } - if ctx != nil { + if ctx != nil && ctx.Done() != nil { hook.done = make(chan struct{}) go hook.wait() }