diff --git a/v2/cmd/wails/internal/commands/generate/template/base/go.sum b/v2/cmd/wails/internal/commands/generate/template/base/go.sum index 63a3181cc..92f4d6d57 100644 --- a/v2/cmd/wails/internal/commands/generate/template/base/go.sum +++ b/v2/cmd/wails/internal/commands/generate/template/base/go.sum @@ -53,8 +53,6 @@ github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oO github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= -github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZylykMwwx/MNEXn9fp+Y= -github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= diff --git a/v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod b/v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod index 1555a4829..fcef41340 100644 --- a/v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod +++ b/v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod @@ -12,7 +12,6 @@ module changeme github.com/labstack/echo/v4 v4.7.2 // indirect github.com/labstack/gommon v0.3.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect diff --git a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.mod.tmpl b/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.mod.tmpl index d3e1921a2..27a2d22f4 100644 --- a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.mod.tmpl +++ b/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.mod.tmpl @@ -18,7 +18,6 @@ github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 // indirect github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect -github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect diff --git a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.sum b/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.sum index 4ba840926..3e14e745f 100644 --- a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.sum +++ b/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/go.sum @@ -81,8 +81,6 @@ github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oO github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= -github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZylykMwwx/MNEXn9fp+Y= -github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= diff --git a/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.mod.tmpl b/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.mod.tmpl index d3e1921a2..27a2d22f4 100644 --- a/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.mod.tmpl +++ b/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.mod.tmpl @@ -18,7 +18,6 @@ github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 // indirect github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect -github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect diff --git a/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.sum b/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.sum index 4ba840926..3e14e745f 100644 --- a/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.sum +++ b/v2/cmd/wails/internal/commands/initialise/templates/templates/plain/go.sum @@ -81,8 +81,6 @@ github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oO github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= -github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZylykMwwx/MNEXn9fp+Y= -github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA= github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= diff --git a/v2/go.mod b/v2/go.mod index 5e3f0c2a8..f8e63331d 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -17,7 +17,6 @@ require ( github.com/leaanthony/clir v1.0.4 github.com/leaanthony/debme v1.2.1 github.com/leaanthony/go-ansi-parser v1.0.1 - github.com/leaanthony/go-common-file-dialog v1.0.3 github.com/leaanthony/gosod v1.0.3 github.com/leaanthony/idgen v1.0.0 github.com/leaanthony/slicer v1.5.0 diff --git a/v2/go.sum b/v2/go.sum index 14ea32693..090c749b6 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -79,8 +79,6 @@ github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oO github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= -github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZylykMwwx/MNEXn9fp+Y= -github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/idgen v1.0.0 h1:IZreR+JGEzFV4yeVuBZA25gM0keUoFy+RDUldncQ+Jw= diff --git a/v2/internal/ffenestri/ffenestri_client_windows.go b/v2/internal/ffenestri/ffenestri_client_windows.go index c00c13706..a9a6cf042 100644 --- a/v2/internal/ffenestri/ffenestri_client_windows.go +++ b/v2/internal/ffenestri/ffenestri_client_windows.go @@ -14,7 +14,7 @@ import ( "strconv" "syscall" - "github.com/leaanthony/go-common-file-dialog/cfd" + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" "github.com/wailsapp/wails/v2/pkg/runtime" "golang.org/x/sys/windows" diff --git a/v2/internal/frontend/desktop/windows/dialog.go b/v2/internal/frontend/desktop/windows/dialog.go index f2bb7bf1d..a8fa5b8a3 100644 --- a/v2/internal/frontend/desktop/windows/dialog.go +++ b/v2/internal/frontend/desktop/windows/dialog.go @@ -4,8 +4,8 @@ package windows import ( - "github.com/leaanthony/go-common-file-dialog/cfd" "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" "golang.org/x/sys/windows" "syscall" ) diff --git a/v2/internal/go-common-file-dialog/LICENSE b/v2/internal/go-common-file-dialog/LICENSE new file mode 100644 index 000000000..508b6978e --- /dev/null +++ b/v2/internal/go-common-file-dialog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Harry Phillips + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/v2/internal/go-common-file-dialog/README.md b/v2/internal/go-common-file-dialog/README.md new file mode 100644 index 000000000..1cb5902d1 --- /dev/null +++ b/v2/internal/go-common-file-dialog/README.md @@ -0,0 +1,31 @@ +# Common File Dialog bindings for Golang + +[Project Home](https://github.com/harry1453/go-common-file-dialog) + +This library contains bindings for Windows Vista and +newer's [Common File Dialogs](https://docs.microsoft.com/en-us/windows/win32/shell/common-file-dialog), which is the +standard system dialog for selecting files or folders to open or save. + +The Common File Dialogs have to be accessed via +the [COM Interface](https://en.wikipedia.org/wiki/Component_Object_Model), normally via C++ or via bindings (like in C#) +. + +This library contains bindings for Golang. **It does not require CGO**, and contains empty stubs for non-windows +platforms (so is safe to compile and run on platforms other than windows, but will just return errors at runtime). + +This can be very useful if you want to quickly get a file selector in your Golang application. The `cfdutil` package +contains utility functions with a single call to open and configure a dialog, and then get the result from it. Examples +for this are in [`_examples/usingutil`](_examples/usingutil). Or, if you want finer control over the dialog's operation, +you can use the base package. Examples for this are in [`_examples/notusingutil`](_examples/notusingutil). + +This library is available under the MIT license. + +Currently supported features: + +* Open File Dialog (to open a single file) +* Open Multiple Files Dialog (to open multiple files) +* Open Folder Dialog +* Save File Dialog +* Dialog "roles" to allow Windows to remember different "last locations" for different types of dialog +* Set dialog Title, Default Folder and Initial Folder +* Set dialog File Filters diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go new file mode 100644 index 000000000..58e97aa4e --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go @@ -0,0 +1,72 @@ +// Cross-platform. + +// Common File Dialogs +package cfd + +type Dialog interface { + // Show the dialog to the user. + // Blocks until the user has closed the dialog. + Show() error + // Sets the dialog's parent window. Use 0 to set the dialog to have no parent window. + SetParentWindowHandle(hwnd uintptr) + // Show the dialog to the user. + // Blocks until the user has closed the dialog and returns their selection. + // Returns an error if the user cancelled the dialog. + // Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead. + ShowAndGetResult() (string, error) + // Sets the title of the dialog window. + SetTitle(title string) error + // Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the + // OS will use to differentiate it from dialogs that are intended for other purposes. + // This means that, for example, a dialog with role "Import" will have a different + // previous location that it will open to than a dialog with role "Open". Can be any string. + SetRole(role string) error + // Sets the folder used as a default if there is not a recently used folder value available + SetDefaultFolder(defaultFolder string) error + // Sets the folder that the dialog always opens to. + // If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder. + SetFolder(folder string) error + // Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt" + // Do not use for the Open Multiple Files dialog. Use GetResults instead. + GetResult() (string, error) + // Sets the file name, I.E. the contents of the file name text box. + // For Select Folder Dialog, sets folder name. + SetFileName(fileName string) error + // Release the resources allocated to this Dialog. + // Should be called when the dialog is finished with. + Release() error +} + +type FileDialog interface { + Dialog + // Set the list of file filters that the user can select. + SetFileFilters(fileFilter []FileFilter) error + // Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called. + SetSelectedFileFilterIndex(index uint) error + // Sets the default extension applied when a user does not provide one as part of the file name. + // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. + // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. + // For Save File Dialog, this extension will be used whenever a user does not specify an extension. + SetDefaultExtension(defaultExtension string) error +} + +type OpenFileDialog interface { + FileDialog +} + +type OpenMultipleFilesDialog interface { + FileDialog + // Show the dialog to the user. + // Blocks until the user has closed the dialog and returns the selected files. + ShowAndGetResults() ([]string, error) + // Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt" + GetResults() ([]string, error) +} + +type SelectFolderDialog interface { + Dialog +} + +type SaveFileDialog interface { // TODO Properties + FileDialog +} diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go new file mode 100644 index 000000000..3ab969850 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go @@ -0,0 +1,28 @@ +//go:build !windows +// +build !windows + +package cfd + +import "fmt" + +var unsupportedError = fmt.Errorf("common file dialogs are only available on windows") + +// TODO doc +func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { + return nil, unsupportedError +} + +// TODO doc +func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { + return nil, unsupportedError +} diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go new file mode 100644 index 000000000..69f46118e --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go @@ -0,0 +1,79 @@ +//go:build windows +// +build windows + +package cfd + +import "github.com/go-ole/go-ole" + +func initialize() { + // Swallow error + _ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE) +} + +// TODO doc +func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + err = openDialog.setIsMultiselect(true) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { + initialize() + + openDialog, err := newIFileOpenDialog() + if err != nil { + return nil, err + } + err = config.apply(openDialog) + if err != nil { + return nil, err + } + err = openDialog.setPickFolders(true) + if err != nil { + return nil, err + } + return openDialog, nil +} + +// TODO doc +func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { + initialize() + + saveDialog, err := newIFileSaveDialog() + if err != nil { + return nil, err + } + err = config.apply(saveDialog) + if err != nil { + return nil, err + } + return saveDialog, nil +} diff --git a/v2/internal/go-common-file-dialog/cfd/DialogConfig.go b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go new file mode 100644 index 000000000..221dbef27 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go @@ -0,0 +1,120 @@ +// Cross-platform. + +package cfd + +type FileFilter struct { + // The display name of the filter (That is shown to the user) + DisplayName string + // The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc. + Pattern string +} + +type DialogConfig struct { + // The title of the dialog + Title string + // The role of the dialog. This is used to derive the dialog's GUID, which the + // OS will use to differentiate it from dialogs that are intended for other purposes. + // This means that, for example, a dialog with role "Import" will have a different + // previous location that it will open to than a dialog with role "Open". Can be any string. + Role string + // The default folder - the folder that is used the first time the user opens it + // (after the first time their last used location is used). + DefaultFolder string + // The initial folder - the folder that the dialog always opens to if not empty. + // If this is not empty, it will override the "default folder" behaviour and + // the dialog will always open to this folder. + Folder string + // The file filters that restrict which types of files the dialog is able to choose. + // Ignored by Select Folder Dialog. + FileFilters []FileFilter + // Sets the initially selected file filter. This is an index of FileFilters. + // Ignored by Select Folder Dialog. + SelectedFileFilterIndex uint + // The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog. + // For the Select Folder Dialog, this sets the initial folder name. + FileName string + // The default extension applied when a user does not provide one as part of the file name. + // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. + // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. + // For Save File Dialog, this extension will be used whenever a user does not specify an extension. + // Ignored by Select Folder Dialog. + DefaultExtension string + // ParentWindowHandle is the handle (HWND) to the parent window of the dialog. + // If left as 0 / nil, the dialog will have no parent window. + ParentWindowHandle uintptr +} + +var defaultFilters = []FileFilter{ + { + DisplayName: "All Files (*.*)", + Pattern: "*.*", + }, +} + +func (config *DialogConfig) apply(dialog Dialog) (err error) { + if config.Title != "" { + err = dialog.SetTitle(config.Title) + if err != nil { + return + } + } + + if config.Role != "" { + err = dialog.SetRole(config.Role) + if err != nil { + return + } + } + + if config.Folder != "" { + err = dialog.SetFolder(config.Folder) + if err != nil { + return + } + } + + if config.DefaultFolder != "" { + err = dialog.SetDefaultFolder(config.DefaultFolder) + if err != nil { + return + } + } + + if config.FileName != "" { + err = dialog.SetFileName(config.FileName) + if err != nil { + return + } + } + + dialog.SetParentWindowHandle(config.ParentWindowHandle) + + if dialog, ok := dialog.(FileDialog); ok { + var fileFilters []FileFilter + if config.FileFilters != nil && len(config.FileFilters) > 0 { + fileFilters = config.FileFilters + } else { + fileFilters = defaultFilters + } + err = dialog.SetFileFilters(fileFilters) + if err != nil { + return + } + + if config.SelectedFileFilterIndex != 0 { + err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex) + if err != nil { + return + } + } + + if config.DefaultExtension != "" { + err = dialog.SetDefaultExtension(config.DefaultExtension) + if err != nil { + return + } + } + } + + return +} diff --git a/v2/internal/go-common-file-dialog/cfd/errors.go b/v2/internal/go-common-file-dialog/cfd/errors.go new file mode 100644 index 000000000..c097c8eb2 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/errors.go @@ -0,0 +1,7 @@ +package cfd + +import "errors" + +var ( + ErrorCancelled = errors.New("cancelled by user") +) diff --git a/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go new file mode 100644 index 000000000..8c82cda2c --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go @@ -0,0 +1,197 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/util" + "syscall" + "unsafe" +) + +var ( + fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}") + fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}") +) + +type iFileOpenDialog struct { + vtbl *iFileOpenDialogVtbl + parentWindowHandle uintptr +} + +type iFileOpenDialogVtbl struct { + iFileDialogVtbl + + GetResults uintptr // func (ppenum **IShellItemArray) HRESULT + GetSelectedItems uintptr +} + +func newIFileOpenDialog() (*iFileOpenDialog, error) { + if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil { + return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil + } else { + return nil, err + } +} + +func (fileOpenDialog *iFileOpenDialog) Show() error { + return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle) +} + +func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) { + fileOpenDialog.parentWindowHandle = hwnd +} + +func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return "", err + } + if isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use ShowAndGetResults for open multiple files dialog") + } + if err := fileOpenDialog.Show(); err != nil { + return "", err + } + return fileOpenDialog.GetResult() +} + +func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return nil, err + } + if !isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use ShowAndGetResult for open single file dialog") + } + if err := fileOpenDialog.Show(); err != nil { + return nil, err + } + return fileOpenDialog.GetResults() +} + +func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error { + return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title) +} + +func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return "", err + } + if isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use GetResults for open multiple files dialog") + } + return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) Release() error { + return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error { + return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) +} + +func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error { + return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) +} + +func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error { + return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter) +} + +func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error { + return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), util.StringToUUID(role)) +} + +// This should only be callable when the user asks for a multi select because +// otherwise they will be given the Dialog interface which does not expose this function. +func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) { + isMultiselect, err := fileOpenDialog.isMultiselect() + if err != nil { + return nil, err + } + if !isMultiselect { + // We should panic as this error is caused by the developer using the library + panic("use GetResult for open single file dialog") + } + return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog)) +} + +func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error { + return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension) +} + +func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error { + return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName) +} + +func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error { + return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index) +} + +func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error { + const FosPickfolders = 0x20 + if pickFolders { + return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) + } else { + return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) + } +} + +const FosAllowMultiselect = 0x200 + +func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) { + options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog)) + if err != nil { + return false, err + } + return options&FosAllowMultiselect != 0, nil +} + +func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error { + if isMultiselect { + return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) + } else { + return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) + } +} + +func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) { + var shellItemArray *iShellItemArray + ret, _, _ := syscall.Syscall(vtbl.GetResults, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(&shellItemArray)), + 0) + return shellItemArray, hresultToError(ret) +} + +func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) { + shellItemArray, err := vtbl.getResults(objPtr) + if err != nil { + return nil, err + } + if shellItemArray == nil { + return nil, ErrorCancelled + } + defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray)) + count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray)) + if err != nil { + return nil, err + } + var results []string + for i := uintptr(0); i < count; i++ { + newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i) + if err != nil { + return nil, err + } + results = append(results, newItem) + } + return results, nil +} diff --git a/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go new file mode 100644 index 000000000..3effeda25 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go @@ -0,0 +1,93 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/util" + "unsafe" +) + +var ( + saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}") + saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}") +) + +type iFileSaveDialog struct { + vtbl *iFileSaveDialogVtbl + parentWindowHandle uintptr +} + +type iFileSaveDialogVtbl struct { + iFileDialogVtbl + + SetSaveAsItem uintptr + SetProperties uintptr + SetCollectedProperties uintptr + GetProperties uintptr + ApplyProperties uintptr +} + +func newIFileSaveDialog() (*iFileSaveDialog, error) { + if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil { + return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil + } else { + return nil, err + } +} + +func (fileSaveDialog *iFileSaveDialog) Show() error { + return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle) +} + +func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) { + fileSaveDialog.parentWindowHandle = hwnd +} + +func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) { + if err := fileSaveDialog.Show(); err != nil { + return "", err + } + return fileSaveDialog.GetResult() +} + +func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error { + return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title) +} + +func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) { + return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog)) +} + +func (fileSaveDialog *iFileSaveDialog) Release() error { + return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog)) +} + +func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error { + return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) +} + +func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error { + return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) +} + +func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error { + return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter) +} + +func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error { + return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), util.StringToUUID(role)) +} + +func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error { + return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension) +} + +func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error { + return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName) +} + +func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error { + return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index) +} diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItem.go b/v2/internal/go-common-file-dialog/cfd/iShellItem.go new file mode 100644 index 000000000..6a747f4d9 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iShellItem.go @@ -0,0 +1,53 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "syscall" + "unsafe" +) + +var ( + procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName") + iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe") +) + +type iShellItem struct { + vtbl *iShellItemVtbl +} + +type iShellItemVtbl struct { + iUnknownVtbl + BindToHandler uintptr + GetParent uintptr + GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT + GetAttributes uintptr + Compare uintptr +} + +func newIShellItem(path string) (*iShellItem, error) { + var shellItem *iShellItem + pathPtr := ole.SysAllocString(path) + ret, _, _ := procSHCreateItemFromParsingName.Call( + uintptr(unsafe.Pointer(pathPtr)), + 0, + uintptr(unsafe.Pointer(iidShellItem)), + uintptr(unsafe.Pointer(&shellItem))) + return shellItem, hresultToError(ret) +} + +func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) { + var ptr *uint16 + ret, _, _ := syscall.Syscall(vtbl.GetDisplayName, + 2, + uintptr(objPtr), + 0x80058000, // SIGDN_FILESYSPATH + uintptr(unsafe.Pointer(&ptr))) + if err := hresultToError(ret); err != nil { + return "", err + } + defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr))) + return ole.LpOleStrToString(ptr), nil +} diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go new file mode 100644 index 000000000..84f26fa20 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go @@ -0,0 +1,67 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "github.com/go-ole/go-ole" + "syscall" + "unsafe" +) + +const ( + iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}" +) + +var ( + iidShellItemArray *ole.GUID +) + +func init() { + iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID) +} + +type iShellItemArray struct { + vtbl *iShellItemArrayVtbl +} + +type iShellItemArrayVtbl struct { + iUnknownVtbl + BindToHandler uintptr + GetPropertyStore uintptr + GetPropertyDescriptionList uintptr + GetAttributes uintptr + GetCount uintptr // func (pdwNumItems *DWORD) HRESULT + GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT + EnumItems uintptr +} + +func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) { + var count uintptr + ret, _, _ := syscall.Syscall(vtbl.GetCount, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(&count)), + 0) + if err := hresultToError(ret); err != nil { + return 0, err + } + return count, nil +} + +func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) { + var shellItem *iShellItem + ret, _, _ := syscall.Syscall(vtbl.GetItemAt, + 2, + uintptr(objPtr), + index, + uintptr(unsafe.Pointer(&shellItem))) + if err := hresultToError(ret); err != nil { + return "", err + } + if shellItem == nil { + return "", ErrorCancelled + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) +} diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommon.go b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go new file mode 100644 index 000000000..21015c27c --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go @@ -0,0 +1,48 @@ +//go:build windows +// +build windows + +package cfd + +type comDlgFilterSpec struct { + pszName *int16 + pszSpec *int16 +} + +type iUnknownVtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type iModalWindowVtbl struct { + iUnknownVtbl + Show uintptr // func (hwndOwner HWND) HRESULT +} + +type iFileDialogVtbl struct { + iModalWindowVtbl + SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT + SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT + GetFileTypeIndex uintptr + Advise uintptr + Unadvise uintptr + SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT + GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT + SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT + SetFolder uintptr // func (psi *IShellItem) HRESULT + GetFolder uintptr + GetCurrentSelection uintptr + SetFileName uintptr // func (pszName LPCWSTR) HRESULT + GetFileName uintptr + SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT + SetOkButtonLabel uintptr + SetFileNameLabel uintptr + GetResult uintptr // func (ppsi **IShellItem) HRESULT + AddPlace uintptr + SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT + // This can only be used from a callback. + Close uintptr + SetClientGuid uintptr // func (guid REFGUID) HRESULT + ClearClientData uintptr + SetFilter uintptr +} diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go new file mode 100644 index 000000000..a92100010 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go @@ -0,0 +1,227 @@ +//go:build windows +// +build windows + +package cfd + +import ( + "fmt" + "github.com/go-ole/go-ole" + "strings" + "syscall" + "unsafe" +) + +func hresultToError(hr uintptr) error { + if hr < 0 { + return ole.NewError(hr) + } + return nil +} + +func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error { + ret, _, _ := syscall.Syscall(vtbl.Release, + 0, + uintptr(objPtr), + 0, + 0) + return hresultToError(ret) +} + +func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error { + ret, _, _ := syscall.Syscall(vtbl.Show, + 1, + uintptr(objPtr), + hwnd, + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error { + cFileTypes := len(filters) + if cFileTypes < 0 { + return fmt.Errorf("must specify at least one filter") + } + comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes) + for i := 0; i < cFileTypes; i++ { + filter := &filters[i] + comDlgFilterSpecs[i] = comDlgFilterSpec{ + pszName: ole.SysAllocString(filter.DisplayName), + pszSpec: ole.SysAllocString(filter.Pattern), + } + } + ret, _, _ := syscall.Syscall(vtbl.SetFileTypes, + 2, + uintptr(objPtr), + uintptr(cFileTypes), + uintptr(unsafe.Pointer(&comDlgFilterSpecs[0]))) + return hresultToError(ret) +} + +// Options are: +// 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 +func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error { + ret, _, _ := syscall.Syscall(vtbl.SetOptions, + 1, + uintptr(objPtr), + uintptr(options), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) { + var options uint32 + ret, _, _ := syscall.Syscall(vtbl.GetOptions, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(&options)), + 0) + return options, hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error { + if options, err := vtbl.getOptions(objPtr); err == nil { + return vtbl.setOptions(objPtr, options|option) + } else { + return err + } +} + +func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error { + if options, err := vtbl.getOptions(objPtr); err == nil { + return vtbl.setOptions(objPtr, options&^option) + } else { + return err + } +} + +func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error { + shellItem, err := newIShellItem(path) + if err != nil { + return err + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + ret, _, _ := syscall.Syscall(vtbl.SetDefaultFolder, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(shellItem)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error { + shellItem, err := newIShellItem(path) + if err != nil { + return err + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + ret, _, _ := syscall.Syscall(vtbl.SetFolder, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(shellItem)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error { + titlePtr := ole.SysAllocString(title) + ret, _, _ := syscall.Syscall(vtbl.SetTitle, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(titlePtr)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error { + ret, _, _ := syscall.Syscall(vtbl.Close, + 1, + uintptr(objPtr), + 0, + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) { + var shellItem *iShellItem + ret, _, _ := syscall.Syscall(vtbl.GetResult, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(&shellItem)), + 0) + return shellItem, hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) { + shellItem, err := vtbl.getResult(objPtr) + if err != nil { + return "", err + } + if shellItem == nil { + return "", ErrorCancelled + } + defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) + return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) +} + +func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error { + ret, _, _ := syscall.Syscall(vtbl.SetClientGuid, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(guid)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error { + if defaultExtension[0] == '.' { + defaultExtension = strings.TrimPrefix(defaultExtension, ".") + } + defaultExtensionPtr := ole.SysAllocString(defaultExtension) + ret, _, _ := syscall.Syscall(vtbl.SetDefaultExtension, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(defaultExtensionPtr)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error { + fileNamePtr := ole.SysAllocString(fileName) + ret, _, _ := syscall.Syscall(vtbl.SetFileName, + 1, + uintptr(objPtr), + uintptr(unsafe.Pointer(fileNamePtr)), + 0) + return hresultToError(ret) +} + +func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error { + ret, _, _ := syscall.Syscall(vtbl.SetFileTypeIndex, + 1, + uintptr(objPtr), + uintptr(index+1), // SetFileTypeIndex counts from 1 + 0) + return hresultToError(ret) +} diff --git a/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go new file mode 100644 index 000000000..bde52d743 --- /dev/null +++ b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go @@ -0,0 +1,45 @@ +package cfdutil + +import ( + "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" +) + +// TODO doc +func ShowOpenFileDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewOpenFileDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} + +// TODO doc +func ShowOpenMultipleFilesDialog(config cfd.DialogConfig) ([]string, error) { + dialog, err := cfd.NewOpenMultipleFilesDialog(config) + if err != nil { + return nil, err + } + defer dialog.Release() + return dialog.ShowAndGetResults() +} + +// TODO doc +func ShowPickFolderDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewSelectFolderDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} + +// TODO doc +func ShowSaveFileDialog(config cfd.DialogConfig) (string, error) { + dialog, err := cfd.NewSaveFileDialog(config) + if err != nil { + return "", err + } + defer dialog.Release() + return dialog.ShowAndGetResult() +} diff --git a/v2/internal/go-common-file-dialog/util/util.go b/v2/internal/go-common-file-dialog/util/util.go new file mode 100644 index 000000000..723fbedc0 --- /dev/null +++ b/v2/internal/go-common-file-dialog/util/util.go @@ -0,0 +1,10 @@ +package util + +import ( + "github.com/go-ole/go-ole" + "github.com/google/uuid" +) + +func StringToUUID(str string) *ole.GUID { + return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String()) +} diff --git a/v2/internal/go-common-file-dialog/util/util_test.go b/v2/internal/go-common-file-dialog/util/util_test.go new file mode 100644 index 000000000..2e8ffeb05 --- /dev/null +++ b/v2/internal/go-common-file-dialog/util/util_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "github.com/go-ole/go-ole" + "testing" +) + +func TestStringToUUID(t *testing.T) { + generated := *StringToUUID("TestTestTest") + expected := *ole.NewGUID("7933985F-2C87-5A5B-A26E-5D0326829AC2") + if generated != expected { + t.Errorf("not equal. expected %s, found %s", expected.String(), generated.String()) + } +} diff --git a/v2/internal/gomod/gomod_test.go b/v2/internal/gomod/gomod_test.go index 5173a2230..43907c4a2 100644 --- a/v2/internal/gomod/gomod_test.go +++ b/v2/internal/gomod/gomod_test.go @@ -28,7 +28,6 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -91,7 +90,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -132,7 +131,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -172,7 +171,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -213,7 +212,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -254,7 +253,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -297,7 +296,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -341,7 +340,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -443,7 +442,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -484,7 +483,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -525,7 +524,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect @@ -569,7 +568,7 @@ require ( github.com/klauspost/compress v1.12.2 // indirect github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/go-ansi-parser v1.0.1 // indirect - github.com/leaanthony/go-common-file-dialog v1.0.3 // indirect + github.com/wailsapp/wails/v2/internal/go-common-file-dialog v1.0.3 // indirect github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2 v0.0.0-20211007092718-65d2f028ef2d // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.5.0 // indirect