diff --git a/internal/w32/comdlg32_windows.go b/internal/w32/comdlg32_windows.go deleted file mode 100644 index 1e33199..0000000 --- a/internal/w32/comdlg32_windows.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, version 2.0. If a copy of the MPL was not distributed with -// this file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// This Source Code Form is "Incompatible With Secondary Licenses", as -// defined by the Mozilla Public License, version 2.0. - -package w32 - -import ( - "syscall" - "unsafe" -) - -var ( - comdlg32 = syscall.NewLazyDLL("comdlg32.dll") - getOpenFileNameWProc = comdlg32.NewProc("GetOpenFileNameW") - getSaveFileNameWProc = comdlg32.NewProc("GetSaveFileNameW") -) - -// Constants from https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamea -const ( - OFNReadOnly = 0x00000001 - OFNOverwritePrompt = 0x00000002 - OFNHideReadOnly = 0x00000004 - OFNNoChangeDir = 0x00000008 - OFNShowHelp = 0x00000010 - OFNEnableHook = 0x00000020 - OFNEnableTemplate = 0x00000040 - OFNEnableTemplateHandle = 0x00000080 - OFNNoValidate = 0x00000100 - OFNAllowMultiSelect = 0x00000200 - OFNExtensionDifferent = 0x00000400 - OFNPathMustExist = 0x00000800 - OFNFileMustExist = 0x00001000 - OFNCreatePrompt = 0x00002000 - OFNShareAware = 0x00004000 - OFNNoReadOnlyReturn = 0x00008000 - OFNNoTestFileCreate = 0x00010000 - OFNNoNetworkButton = 0x00020000 - OFNNoLongNames = 0x00040000 - OFNExplorer = 0x00080000 - OFNNoDereferenceLinks = 0x00100000 - OFNLongNames = 0x00200000 - OFNEnableIncludeNotify = 0x00400000 - OFNEnableSizing = 0x00800000 - OFNDontAddToRecent = 0x02000000 - OFNForceShowHidden = 0x10000000 -) - -// OpenFileName https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew -//nolint:maligned // Can't do anything about Windows structs being poorly aligned -type OpenFileName struct { - Size uint32 - Owner HWND - Instance syscall.Handle - Filter uintptr - CustomFilter uintptr - MaxCustomFilter uint32 - FilterIndex uint32 - FileName uintptr - MaxFileName uint32 - FileTitle uintptr - MaxFileTitle uint32 - InitialDir uintptr - Title uintptr - Flags uint32 - FileOffset uint16 - FileExtension uint16 - DefExt uintptr - CustData uintptr - Hook uintptr - TemplateName uintptr - Reserved1 uintptr - Reserved2 uint32 - FlagsEx uint32 -} - -// GetOpenFileName https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getopenfilenamew -func GetOpenFileName(ofn *OpenFileName) bool { - b, _, _ := getOpenFileNameWProc.Call(uintptr(unsafe.Pointer(ofn))) - return b != 0 -} - -// GetSaveFileName https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getsavefilenamew -func GetSaveFileName(ofn *OpenFileName) bool { - b, _, _ := getSaveFileNameWProc.Call(uintptr(unsafe.Pointer(ofn))) - return b != 0 -} diff --git a/internal/w32/file_dialog_windows.go b/internal/w32/file_dialog_windows.go new file mode 100644 index 0000000..129b3a1 --- /dev/null +++ b/internal/w32/file_dialog_windows.go @@ -0,0 +1,133 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "strings" + "syscall" + "unsafe" +) + +const ( + FOSOverwritePrompt = 0x00000002 + FOSStrictFileTypes = 0x00000004 + FOSNoChangeDir = 0x00000008 + FOSPickFolders = 0x00000020 + FOSForceFileSystem = 0x00000040 + FOSAllNonStorageItems = 0x00000080 + FOSNoValidate = 0x00000100 + FOSAllowMultiSelect = 0x00000200 + FOSPathMustExist = 0x00000800 + FOSFileMustExist = 0x00001000 + FOSCreatePrompt = 0x00002000 + FOSShareAware = 0x00004000 + FOSNoReadOnlyReturn = 0x00008000 + FOSNoTestFileCreate = 0x00010000 + FOSHideMRUPlaces = 0x00020000 + FOSHidePinnedPlaces = 0x00040000 + FOSNoDereferenceLinks = 0x00100000 + FOSOKBUttonNeedsInteraction = 0x00200000 + FOSDontAddToRecent = 0x02000000 + FOSForceShowHidden = 0x10000000 + FOSDefaultNoMiniMode = 0x20000000 + FOSForcePreviewPaneOn = 0x40000000 + FOSSupportsStreamableItems = 0x80000000 +) + +type FileFilter struct { + Name string + Pattern string +} + +type filterSpec struct { + name *int16 + pattern *int16 +} + +type FileDialog struct { + ModalWindow +} + +type vmtFileDialog struct { + vmtModalWindow + SetFileTypes uintptr + SetFileTypeIndex uintptr + GetFileTypeIndex uintptr + Advise uintptr + Unadvise uintptr + SetOptions uintptr + GetOptions uintptr + SetDefaultFolder uintptr + SetFolder uintptr + GetFolder uintptr + GetCurrentSelection uintptr + SetFileName uintptr + GetFileName uintptr + SetTitle uintptr + SetOkButtonLabel uintptr + SetFileNameLabel uintptr + GetResult uintptr + AddPlace uintptr + SetDefaultExtension uintptr + Close uintptr + SetClientGuid uintptr + ClearClientData uintptr + SetFilter uintptr +} + +func (obj *FileDialog) vmt() *vmtFileDialog { + return (*vmtFileDialog)(obj.UnsafeVirtualMethodTable) +} + +func (obj *FileDialog) SetFolder(path string) { + if item := NewShellItem(path); item != nil { + defer item.Release() + syscall.SyscallN(obj.vmt().SetFolder, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(item))) + } +} + +func (obj *FileDialog) SetOptions(options int) { + syscall.SyscallN(obj.vmt().SetOptions, uintptr(unsafe.Pointer(obj)), uintptr(options)) +} + +func (obj *FileDialog) SetFileTypes(filters []FileFilter) { + if len(filters) == 0 { + return + } + specs := make([]filterSpec, len(filters)) + for i, one := range filters { + specs[i] = filterSpec{ + name: SysAllocString(one.Name), + pattern: SysAllocString(one.Pattern), + } + } + syscall.SyscallN(obj.vmt().SetFileTypes, uintptr(unsafe.Pointer(obj)), uintptr(len(specs)), + uintptr(unsafe.Pointer(&specs[0]))) +} + +func (obj *FileDialog) SetDefaultExtension(ext string) { + syscall.SyscallN(obj.vmt().SetDefaultExtension, uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(SysAllocString(strings.TrimPrefix(ext, "."))))) +} + +func (obj *FileDialog) SetFileName(fileName string) { + syscall.SyscallN(obj.vmt().SetFileName, uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(SysAllocString(fileName)))) +} + +func (obj *FileDialog) GetResult() string { + var item *ShellItem + r1, _, _ := syscall.SyscallN(obj.vmt().GetResult, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(&item))) + if r1 != 0 || item == nil { + return "" + } + defer item.Release() + return item.DisplayName() +} diff --git a/internal/w32/guid_windows.go b/internal/w32/guid_windows.go new file mode 100644 index 0000000..56283ab --- /dev/null +++ b/internal/w32/guid_windows.go @@ -0,0 +1,155 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +const hexTable = "0123456789ABCDEF" + +var NullGUID GUID + +// GUID holds a Windows universal ID. +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +// NewGUID creates a GUID from a string. The string may be in one of these formats: +// +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} +// XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +func NewGUID(guid string) GUID { + d := []byte(guid) + var d1, d2, d3, d4a, d4b []byte + switch len(d) { + case 38: + if d[0] != '{' || d[37] != '}' { + return NullGUID + } + d = d[1:37] + fallthrough + case 36: + if d[8] != '-' || d[13] != '-' || d[18] != '-' || d[23] != '-' { + return NullGUID + } + d1 = d[0:8] + d2 = d[9:13] + d3 = d[14:18] + d4a = d[19:23] + d4b = d[24:36] + case 32: + d1 = d[0:8] + d2 = d[8:12] + d3 = d[12:16] + d4a = d[16:20] + d4b = d[20:32] + default: + return NullGUID + } + var g GUID + var ok1, ok2, ok3, ok4 bool + g.Data1, ok1 = decodeHexUint32(d1) + g.Data2, ok2 = decodeHexUint16(d2) + g.Data3, ok3 = decodeHexUint16(d3) + g.Data4, ok4 = decodeHexByte64(d4a, d4b) + if ok1 && ok2 && ok3 && ok4 { + return g + } + return NullGUID +} + +// String returns the string representation of the GUID in its canonical format of +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}. +func (guid GUID) String() string { + var c [38]byte + c[0] = '{' + putUint32Hex(c[1:9], guid.Data1) + c[9] = '-' + putUint16Hex(c[10:14], guid.Data2) + c[14] = '-' + putUint16Hex(c[15:19], guid.Data3) + c[19] = '-' + putByteHex(c[20:24], guid.Data4[0:2]) + c[24] = '-' + putByteHex(c[25:37], guid.Data4[2:8]) + c[37] = '}' + return string(c[:]) +} + +func decodeHexUint32(src []byte) (uint32, bool) { + b1, ok1 := decodeHexByte(src[0], src[1]) + b2, ok2 := decodeHexByte(src[2], src[3]) + b3, ok3 := decodeHexByte(src[4], src[5]) + b4, ok4 := decodeHexByte(src[6], src[7]) + return (uint32(b1) << 24) | (uint32(b2) << 16) | (uint32(b3) << 8) | uint32(b4), ok1 && ok2 && ok3 && ok4 +} + +func decodeHexUint16(src []byte) (uint16, bool) { + b1, ok1 := decodeHexByte(src[0], src[1]) + b2, ok2 := decodeHexByte(src[2], src[3]) + return (uint16(b1) << 8) | uint16(b2), ok1 && ok2 +} + +func decodeHexByte64(s1 []byte, s2 []byte) (value [8]byte, ok bool) { + var ok1, ok2, ok3, ok4, ok5, ok6, ok7, ok8 bool + value[0], ok1 = decodeHexByte(s1[0], s1[1]) + value[1], ok2 = decodeHexByte(s1[2], s1[3]) + value[2], ok3 = decodeHexByte(s2[0], s2[1]) + value[3], ok4 = decodeHexByte(s2[2], s2[3]) + value[4], ok5 = decodeHexByte(s2[4], s2[5]) + value[5], ok6 = decodeHexByte(s2[6], s2[7]) + value[6], ok7 = decodeHexByte(s2[8], s2[9]) + value[7], ok8 = decodeHexByte(s2[10], s2[11]) + return value, ok1 && ok2 && ok3 && ok4 && ok5 && ok6 && ok7 && ok8 +} + +func decodeHexByte(c1, c2 byte) (byte, bool) { + n1, ok1 := decodeHexChar(c1) + n2, ok2 := decodeHexChar(c2) + return (n1 << 4) | n2, ok1 && ok2 +} + +func decodeHexChar(c byte) (byte, bool) { + switch { + case '0' <= c && c <= '9': + return c - '0', true + case 'a' <= c && c <= 'f': + return c - 'a' + 10, true + case 'A' <= c && c <= 'F': + return c - 'A' + 10, true + } + return 0, false +} + +func putUint32Hex(b []byte, v uint32) { + b[0] = hexTable[byte(v>>24)>>4] + b[1] = hexTable[byte(v>>24)&0x0f] + b[2] = hexTable[byte(v>>16)>>4] + b[3] = hexTable[byte(v>>16)&0x0f] + b[4] = hexTable[byte(v>>8)>>4] + b[5] = hexTable[byte(v>>8)&0x0f] + b[6] = hexTable[byte(v)>>4] + b[7] = hexTable[byte(v)&0x0f] +} + +func putUint16Hex(b []byte, v uint16) { + b[0] = hexTable[byte(v>>8)>>4] + b[1] = hexTable[byte(v>>8)&0x0f] + b[2] = hexTable[byte(v)>>4] + b[3] = hexTable[byte(v)&0x0f] +} + +func putByteHex(dst, src []byte) { + for i := 0; i < len(src); i++ { + dst[i*2] = hexTable[src[i]>>4] + dst[i*2+1] = hexTable[src[i]&0x0f] + } +} diff --git a/internal/w32/modal_windows.go b/internal/w32/modal_windows.go new file mode 100644 index 0000000..2d1e300 --- /dev/null +++ b/internal/w32/modal_windows.go @@ -0,0 +1,33 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" +) + +type ModalWindow struct { + Unknown +} + +type vmtModalWindow struct { + vmtUnknown + Show uintptr +} + +func (obj *ModalWindow) vmt() *vmtModalWindow { + return (*vmtModalWindow)(obj.UnsafeVirtualMethodTable) +} + +func (obj *ModalWindow) Show() bool { + r1, _, _ := syscall.SyscallN(obj.vmt().Show, uintptr(unsafe.Pointer(obj)), 0) + return r1 == 0 +} diff --git a/internal/w32/ole32_windows.go b/internal/w32/ole32_windows.go new file mode 100644 index 0000000..d43e028 --- /dev/null +++ b/internal/w32/ole32_windows.go @@ -0,0 +1,46 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + ole32 = syscall.NewLazyDLL("ole32.dll") + coInitializeExProc = ole32.NewProc("CoInitializeEx") + coCreateInstanceProc = ole32.NewProc("CoCreateInstance") + coTaskMemFreeProc = ole32.NewProc("CoTaskMemFree") + instanceIDUnknown = NewGUID("00000000-0000-0000-C000-000000000046") +) + +func CoInitialize(coInit int) { + coInitializeExProc.Call(0, uintptr(coInit)) +} + +func CoCreateInstance(classID, instanceID GUID) *Unknown { + if instanceID == NullGUID { + instanceID = instanceIDUnknown + } + var unknown *Unknown + if r1, _, _ := coCreateInstanceProc.Call(uintptr(unsafe.Pointer(&classID)), 0, + windows.CLSCTX_INPROC_SERVER|windows.CLSCTX_LOCAL_SERVER|windows.CLSCTX_REMOTE_SERVER, + uintptr(unsafe.Pointer(&instanceID)), uintptr(unsafe.Pointer(&unknown))); r1 != 0 { + return nil + } + return unknown +} + +func CoTaskMemFree(ptr uintptr) { + coTaskMemFreeProc.Call(ptr) +} diff --git a/internal/w32/oleaut32_windows.go b/internal/w32/oleaut32_windows.go new file mode 100644 index 0000000..8d0776d --- /dev/null +++ b/internal/w32/oleaut32_windows.go @@ -0,0 +1,29 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + oleaut32 = syscall.NewLazyDLL("oleaut32.dll") + sysAllocStringProc = oleaut32.NewProc("SysAllocString") +) + +func SysAllocString(str string) *int16 { + p, err := syscall.UTF16PtrFromString(str) + if err != nil { + return nil + } + r1, _, _ := sysAllocStringProc.Call(uintptr(unsafe.Pointer(p))) + return (*int16)(unsafe.Pointer(r1)) +} diff --git a/internal/w32/open_dialog_windows.go b/internal/w32/open_dialog_windows.go new file mode 100644 index 0000000..ebc8f87 --- /dev/null +++ b/internal/w32/open_dialog_windows.go @@ -0,0 +1,55 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + fileOpenDialogCLSID = NewGUID("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7") + fileOpenDialogIID = NewGUID("D57C7288-D4AD-4768-BE02-9D969532D960") +) + +type FileOpenDialog struct { + FileDialog +} + +type vmtFileOpenDialog struct { + vmtFileDialog + GetResults uintptr + GetSelectedItems uintptr +} + +func (obj *FileOpenDialog) vmt() *vmtFileOpenDialog { + return (*vmtFileOpenDialog)(obj.UnsafeVirtualMethodTable) +} + +func NewOpenDialog() *FileOpenDialog { + CoInitialize(windows.COINIT_MULTITHREADED | windows.COINIT_DISABLE_OLE1DDE) + return (*FileOpenDialog)(unsafe.Pointer(CoCreateInstance(fileOpenDialogCLSID, fileOpenDialogIID))) +} + +func (obj *FileOpenDialog) GetResults() []string { + var array *ShellItemArray + r1, _, _ := syscall.SyscallN(obj.vmt().GetResults, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(&array))) + if r1 != 0 { + return nil + } + defer array.Release() + s := make([]string, array.Count()) + for i := range s { + s[i] = array.Item(i) + } + return s +} diff --git a/internal/w32/save_dialog_windows.go b/internal/w32/save_dialog_windows.go new file mode 100644 index 0000000..2ba0dd6 --- /dev/null +++ b/internal/w32/save_dialog_windows.go @@ -0,0 +1,43 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + fileSaveDialogCLSID = NewGUID("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B") + fileSaveDialogIID = NewGUID("84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB") +) + +type FileSaveDialog struct { + FileDialog +} + +type vmtFileSaveDialog struct { + vmtFileDialog + SetSaveAsItem uintptr + SetProperties uintptr + SetCollectedProperties uintptr + GetProperties uintptr + ApplyProperties uintptr +} + +func (obj *FileSaveDialog) vmt() *vmtFileSaveDialog { + return (*vmtFileSaveDialog)(obj.UnsafeVirtualMethodTable) +} + +func NewSaveDialog() *FileSaveDialog { + CoInitialize(windows.COINIT_MULTITHREADED | windows.COINIT_DISABLE_OLE1DDE) + return (*FileSaveDialog)(unsafe.Pointer(CoCreateInstance(fileSaveDialogCLSID, fileSaveDialogIID))) +} diff --git a/internal/w32/shell_item_windows.go b/internal/w32/shell_item_windows.go new file mode 100644 index 0000000..3f22533 --- /dev/null +++ b/internal/w32/shell_item_windows.go @@ -0,0 +1,99 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" +) + +const SIGDN_FILESYSPATH = 0x80058000 + +var ( + shell32 = syscall.NewLazyDLL("Shell32.dll") + shCreateItemFromParsingNameProc = shell32.NewProc("SHCreateItemFromParsingName") + shellItemIID = NewGUID("43826D1E-E718-42EE-BC55-A1E261C37BFE") +) + +type ShellItem struct { + Unknown +} + +type vmtShellItem struct { + vmtUnknown + BindToHandler uintptr + GetParent uintptr + GetDisplayName uintptr + GetAttributes uintptr + Compare uintptr +} + +func NewShellItem(path string) *ShellItem { + var item *ShellItem + if r1, _, _ := shCreateItemFromParsingNameProc.Call(uintptr(unsafe.Pointer(SysAllocString(path))), 0, + uintptr(unsafe.Pointer(&shellItemIID)), uintptr(unsafe.Pointer(&item))); r1 != 0 { + return nil + } + return item +} + +func (obj *ShellItem) vmt() *vmtShellItem { + return (*vmtShellItem)(obj.UnsafeVirtualMethodTable) +} + +func (obj *ShellItem) DisplayName() string { + var p *uint16 + r1, _, _ := syscall.SyscallN(obj.vmt().GetDisplayName, uintptr(unsafe.Pointer(obj)), SIGDN_FILESYSPATH, + uintptr(unsafe.Pointer(&p))) + if r1 != 0 { + return "" + } + defer CoTaskMemFree(uintptr(unsafe.Pointer(p))) + return syscall.UTF16ToString((*[1 << 30]uint16)(unsafe.Pointer(p))[:]) +} + +type ShellItemArray struct { + Unknown +} + +type vmtShellItemArray struct { + vmtUnknown + BindToHandler uintptr + GetPropertyStore uintptr + GetPropertyDescriptionList uintptr + GetAttributes uintptr + GetCount uintptr + GetItemAt uintptr + EnumItems uintptr +} + +func (obj *ShellItemArray) vmt() *vmtShellItemArray { + return (*vmtShellItemArray)(obj.UnsafeVirtualMethodTable) +} + +func (obj *ShellItemArray) Count() int { + var count uintptr + r1, _, _ := syscall.SyscallN(obj.vmt().GetCount, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(&count))) + if r1 != 0 { + return 0 + } + return int(count) +} + +func (obj *ShellItemArray) Item(index int) string { + var item *ShellItem + r1, _, _ := syscall.SyscallN(obj.vmt().GetItemAt, uintptr(unsafe.Pointer(obj)), uintptr(index), + uintptr(unsafe.Pointer(&item))) + if r1 != 0 { + return "" + } + defer item.Release() + return item.DisplayName() +} diff --git a/internal/w32/unknown_windows.go b/internal/w32/unknown_windows.go new file mode 100644 index 0000000..9b43dcf --- /dev/null +++ b/internal/w32/unknown_windows.go @@ -0,0 +1,50 @@ +// Copyright ©2021-2022 by Richard A. Wilkes. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, version 2.0. If a copy of the MPL was not distributed with +// this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, version 2.0. + +package w32 + +import ( + "syscall" + "unsafe" +) + +// Unknown https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown +type Unknown struct { + UnsafeVirtualMethodTable unsafe.Pointer +} + +type vmtUnknown struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +func (obj *Unknown) vmt() *vmtUnknown { + return (*vmtUnknown)(obj.UnsafeVirtualMethodTable) +} + +// QueryInterface https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void) +func (obj *Unknown) QueryInterface(guid *GUID) unsafe.Pointer { + var dest unsafe.Pointer + if ret, _, _ := syscall.SyscallN(obj.vmt().QueryInterface, uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(&dest))); ret != 0 { + return nil + } + return dest +} + +// AddRef https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref +func (obj *Unknown) AddRef() { + syscall.SyscallN(obj.vmt().AddRef, uintptr(unsafe.Pointer(obj))) +} + +// Release https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release +func (obj *Unknown) Release() { + syscall.SyscallN(obj.vmt().Release, uintptr(unsafe.Pointer(obj))) +} diff --git a/open_dialog_windows.go b/open_dialog_windows.go index 68809ca..7aca29d 100644 --- a/open_dialog_windows.go +++ b/open_dialog_windows.go @@ -12,10 +12,9 @@ package unison import ( "path/filepath" "strings" - "unicode/utf16" - "unsafe" "github.com/richardwilkes/toolbox/i18n" + "github.com/richardwilkes/toolbox/log/jot" "github.com/richardwilkes/unison/internal/w32" ) @@ -39,38 +38,32 @@ func (d *winOpenDialog) RunModal() bool { active.ToFront() } }() - var fileNameBuffer [64 * 1024]uint16 - filter := createExtensionFilter(d.extensions) - initialDir := utf16.Encode([]rune(d.initialDir + "\x00")) - ofn := w32.OpenFileName{ - Size: uint32(unsafe.Sizeof(w32.OpenFileName{})), - FileName: uintptr(unsafe.Pointer(&fileNameBuffer[0])), - MaxFileName: uint32(len(fileNameBuffer)), - Filter: uintptr(unsafe.Pointer(&filter[0])), - FilterIndex: 1, - InitialDir: uintptr(unsafe.Pointer(&initialDir[0])), - Flags: w32.OFNExplorer | w32.OFNPathMustExist | w32.OFNFileMustExist, + + openDialog := w32.NewOpenDialog() + if openDialog == nil { + jot.Error("unable to create open dialog") + return false + } + if d.initialDir != "" { + openDialog.SetFolder(filepath.Clean(d.initialDir)) + } + options := w32.FOSPathMustExist | w32.FOSFileMustExist + if d.canChooseDirs { + options |= w32.FOSPickFolders } if d.allowMultipleSelection { - ofn.Flags |= w32.OFNAllowMultiSelect + options |= w32.FOSAllowMultiSelect } if !d.resolvesAliases { - ofn.Flags |= w32.OFNNoDereferenceLinks + options |= w32.FOSNoDereferenceLinks } + openDialog.SetOptions(options) + openDialog.SetFileTypes(d.createFilters()) d.paths = nil - if !w32.GetOpenFileName(&ofn) { + if !openDialog.Show() { return false } - start := 0 - for i := range fileNameBuffer { - if fileNameBuffer[i] == 0 { - if start == i { - break - } - d.paths = append(d.paths, string(utf16.Decode(fileNameBuffer[start:i]))) - start = i + 1 - } - } + d.paths = openDialog.GetResults() switch len(d.paths) { case 0: return false @@ -96,44 +89,38 @@ func (d *winOpenDialog) RunModal() bool { return true } -func createExtensionFilter(extensions []string) []uint16 { - if len(extensions) == 0 { - extensions = []string{"*"} - } - readable := make([]string, 0, len(extensions)) - for _, ext := range extensions { +func (d *winOpenDialog) createFilters() []w32.FileFilter { + filters := make([]w32.FileFilter, 0, len(d.extensions)+1) + readable := make([]string, 0, len(d.extensions)) + for _, ext := range d.extensions { if ext != "*" { - readable = append(readable, ext) + readable = append(readable, "*."+ext) } } - var buffer strings.Builder - if len(readable) > 1 { - buffer.WriteString(i18n.Text("All Readable Files")) - buffer.WriteByte(0) - for i, ext := range readable { - if i != 0 { - buffer.WriteString(";") - } - buffer.WriteString("*.") - buffer.WriteString(ext) - } - buffer.WriteByte(0) + if len(readable) != 0 { + filters = append(filters, w32.FileFilter{ + Name: i18n.Text("All Readable Files"), + Pattern: strings.Join(readable, ";"), + }) } - for _, ext := range extensions { + for _, ext := range d.extensions { if ext == "*" { - buffer.WriteString(i18n.Text("All Files")) - buffer.WriteByte(0) - buffer.WriteString("*.*") - buffer.WriteByte(0) + filters = append(filters, w32.FileFilter{ + Name: i18n.Text("All Files"), + Pattern: "*.*", + }) } else { - buffer.WriteString(ext) - buffer.WriteString(i18n.Text(" Files")) - buffer.WriteByte(0) - buffer.WriteString("*.") - buffer.WriteString(ext) - buffer.WriteByte(0) + filters = append(filters, w32.FileFilter{ + Name: ext + i18n.Text(" Files"), + Pattern: "*." + ext, + }) } } - buffer.WriteByte(0) - return utf16.Encode([]rune(buffer.String())) + if len(d.extensions) == 0 { + filters = append(filters, w32.FileFilter{ + Name: i18n.Text("All Files"), + Pattern: "*.*", + }) + } + return filters } diff --git a/save_dialog_windows.go b/save_dialog_windows.go index 79aa00b..b377deb 100644 --- a/save_dialog_windows.go +++ b/save_dialog_windows.go @@ -11,9 +11,9 @@ package unison import ( "path/filepath" - "unicode/utf16" - "unsafe" + "github.com/richardwilkes/toolbox/i18n" + "github.com/richardwilkes/toolbox/log/jot" "github.com/richardwilkes/unison/internal/w32" ) @@ -37,29 +37,53 @@ func (d *winSaveDialog) RunModal() bool { active.ToFront() } }() - var fileNameBuffer [64 * 1024]uint16 - filter := createExtensionFilter(d.extensions) - initialDir := utf16.Encode([]rune(d.initialDir + "\x00")) - d.paths = nil - if !w32.GetSaveFileName(&w32.OpenFileName{ - Size: uint32(unsafe.Sizeof(w32.OpenFileName{})), - FileName: uintptr(unsafe.Pointer(&fileNameBuffer[0])), - MaxFileName: uint32(len(fileNameBuffer)), - Filter: uintptr(unsafe.Pointer(&filter[0])), - FilterIndex: 1, - InitialDir: uintptr(unsafe.Pointer(&initialDir[0])), - Flags: w32.OFNExplorer | w32.OFNPathMustExist | w32.OFNNoTestFileCreate | w32.OFNOverwritePrompt, - }) { + + saveDialog := w32.NewSaveDialog() + if saveDialog == nil { + jot.Error("unable to create save dialog") return false } - for i := range fileNameBuffer { - if fileNameBuffer[i] == 0 { - d.paths = append(d.paths, string(utf16.Decode(fileNameBuffer[:i]))) + if d.initialDir != "" { + saveDialog.SetFolder(filepath.Clean(d.initialDir)) + } + options := w32.FOSOverwritePrompt | w32.FOSPathMustExist | w32.FOSNoTestFileCreate + if d.canChooseDirs { + options |= w32.FOSPickFolders + } + if d.allowMultipleSelection { + options |= w32.FOSAllowMultiSelect + } + if !d.resolvesAliases { + options |= w32.FOSNoDereferenceLinks + } + saveDialog.SetOptions(options) + saveDialog.SetFileTypes(d.createFilters()) + for _, ext := range d.extensions { + if ext != "*" { + saveDialog.SetDefaultExtension(ext) break } } - if len(d.paths) != 0 { - lastWorkingDir = filepath.Dir(d.paths[0]) + d.paths = nil + if !saveDialog.Show() { + return false + } + result := saveDialog.GetResult() + if result == "" { + return false } + d.paths = []string{result} + lastWorkingDir = filepath.Dir(d.paths[0]) return true } + +func (d *winSaveDialog) createFilters() []w32.FileFilter { + filters := make([]w32.FileFilter, 0, len(d.extensions)) + for _, ext := range d.extensions { + filters = append(filters, w32.FileFilter{ + Name: ext + i18n.Text(" Files"), + Pattern: "*." + ext, + }) + } + return filters +}