mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-10 04:50:07 +08:00
[v3 windows] initial dialog support. Refactor button callback name
This commit is contained in:
parent
2fbb21a84e
commit
a23bb1e350
@ -59,6 +59,24 @@ TBD
|
||||
|
||||
Dialogs are now available in JavaScript!
|
||||
|
||||
### Windows
|
||||
|
||||
Dialog buttons in Windows are not configurable and are constant depending on the type of dialog. To trigger a callback when a button is pressed, create a button with the same name as the button you wish to have the callback attached to.
|
||||
Example: Create a button with the label `Ok` and use `OnClick()` to set the callback method:
|
||||
```go
|
||||
dialog := app.QuestionDialog().
|
||||
SetTitle("Update").
|
||||
SetMessage("The cancel button is selected when pressing escape")
|
||||
ok := dialog.AddButton("Ok")
|
||||
ok.OnClick(func() {
|
||||
// Do something
|
||||
})
|
||||
no := dialog.AddButton("Cancel")
|
||||
dialog.SetDefaultButton(ok)
|
||||
dialog.SetCancelButton(no)
|
||||
dialog.Show()
|
||||
```
|
||||
|
||||
## Drag and Drop
|
||||
|
||||
Native drag and drop can be enabled per-window. Simply set the `EnableDragAndDrop` window config option to `true` and the window will allow files to be dragged onto it. When this happens, the `events.FilesDropped` event will be emitted. The filenames can then be retrieved from the WindowEventContext using the `DroppedFiles()` method. This returns a slice of strings containing the filenames.
|
||||
|
@ -20,6 +20,7 @@ func main() {
|
||||
ApplicationShouldTerminateAfterLastWindowClosed: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Create a custom menu
|
||||
menu := app.NewMenu()
|
||||
menu.AddRole(application.AppMenu)
|
||||
|
@ -36,12 +36,27 @@ func main() {
|
||||
myMenu := app.NewMenu()
|
||||
myMenu.Add("Hello World!").OnClick(func(ctx *application.Context) {
|
||||
println("Hello World!")
|
||||
// app.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show()
|
||||
q := app.QuestionDialog().SetTitle("Ready?").SetMessage("Are you feeling ready?")
|
||||
q.AddButton("Yes").OnClick(func() {
|
||||
println("Awesome!")
|
||||
})
|
||||
q.AddButton("No").SetAsDefault().OnClick(func() {
|
||||
println("Boo!")
|
||||
})
|
||||
q.Show()
|
||||
})
|
||||
subMenu := myMenu.AddSubmenu("Submenu")
|
||||
subMenu.Add("Click me!").OnClick(func(ctx *application.Context) {
|
||||
ctx.ClickedMenuItem().SetLabel("Clicked!")
|
||||
})
|
||||
myMenu.AddSeparator()
|
||||
myMenu.AddCheckbox("Checked", true).OnClick(func(ctx *application.Context) {
|
||||
println("Checked: ", ctx.ClickedMenuItem().Checked())
|
||||
app.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show()
|
||||
})
|
||||
myMenu.Add("Enabled").OnClick(func(ctx *application.Context) {
|
||||
println("Click me!")
|
||||
// ctx.ClickedMenuItem().SetLabel("Clicked!")
|
||||
ctx.ClickedMenuItem().SetLabel("Disabled!").SetEnabled(false)
|
||||
})
|
||||
myMenu.AddSeparator()
|
||||
myMenu.Add("Quit").OnClick(func(ctx *application.Context) {
|
||||
|
21
v3/internal/go-common-file-dialog/LICENSE
Normal file
21
v3/internal/go-common-file-dialog/LICENSE
Normal file
@ -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.
|
31
v3/internal/go-common-file-dialog/README.md
Normal file
31
v3/internal/go-common-file-dialog/README.md
Normal file
@ -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
|
72
v3/internal/go-common-file-dialog/cfd/CommonFileDialog.go
Normal file
72
v3/internal/go-common-file-dialog/cfd/CommonFileDialog.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
120
v3/internal/go-common-file-dialog/cfd/DialogConfig.go
Normal file
120
v3/internal/go-common-file-dialog/cfd/DialogConfig.go
Normal file
@ -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
|
||||
}
|
7
v3/internal/go-common-file-dialog/cfd/errors.go
Normal file
7
v3/internal/go-common-file-dialog/cfd/errors.go
Normal file
@ -0,0 +1,7 @@
|
||||
package cfd
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorCancelled = errors.New("cancelled by user")
|
||||
)
|
201
v3/internal/go-common-file-dialog/cfd/iFileOpenDialog.go
Normal file
201
v3/internal/go-common-file-dialog/cfd/iFileOpenDialog.go
Normal file
@ -0,0 +1,201 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package cfd
|
||||
|
||||
import (
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/google/uuid"
|
||||
"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), 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
|
||||
}
|
||||
|
||||
func StringToUUID(str string) *ole.GUID {
|
||||
return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String())
|
||||
}
|
92
v3/internal/go-common-file-dialog/cfd/iFileSaveDialog.go
Normal file
92
v3/internal/go-common-file-dialog/cfd/iFileSaveDialog.go
Normal file
@ -0,0 +1,92 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package cfd
|
||||
|
||||
import (
|
||||
"github.com/go-ole/go-ole"
|
||||
"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), 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)
|
||||
}
|
53
v3/internal/go-common-file-dialog/cfd/iShellItem.go
Normal file
53
v3/internal/go-common-file-dialog/cfd/iShellItem.go
Normal file
@ -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
|
||||
}
|
67
v3/internal/go-common-file-dialog/cfd/iShellItemArray.go
Normal file
67
v3/internal/go-common-file-dialog/cfd/iShellItemArray.go
Normal file
@ -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))
|
||||
}
|
48
v3/internal/go-common-file-dialog/cfd/vtblCommon.go
Normal file
48
v3/internal/go-common-file-dialog/cfd/vtblCommon.go
Normal file
@ -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
|
||||
}
|
227
v3/internal/go-common-file-dialog/cfd/vtblCommonFunc.go
Normal file
227
v3/internal/go-common-file-dialog/cfd/vtblCommonFunc.go
Normal file
@ -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)
|
||||
}
|
45
v3/internal/go-common-file-dialog/cfdutil/CFDUtil.go
Normal file
45
v3/internal/go-common-file-dialog/cfdutil/CFDUtil.go
Normal file
@ -0,0 +1,45 @@
|
||||
package cfdutil
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/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()
|
||||
}
|
10
v3/internal/go-common-file-dialog/util/util.go
Normal file
10
v3/internal/go-common-file-dialog/util/util.go
Normal file
@ -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())
|
||||
}
|
14
v3/internal/go-common-file-dialog/util/util_test.go
Normal file
14
v3/internal/go-common-file-dialog/util/util_test.go
Normal file
@ -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())
|
||||
}
|
||||
}
|
@ -48,11 +48,22 @@ type Button struct {
|
||||
Label string
|
||||
IsCancel bool
|
||||
IsDefault bool
|
||||
callback func()
|
||||
Callback func()
|
||||
}
|
||||
|
||||
func (b *Button) OnClick(callback func()) {
|
||||
b.callback = callback
|
||||
func (b *Button) OnClick(callback func()) *Button {
|
||||
b.Callback = callback
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Button) SetAsDefault() *Button {
|
||||
b.IsDefault = true
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Button) SetAsCancel() *Button {
|
||||
b.IsCancel = true
|
||||
return b
|
||||
}
|
||||
|
||||
type messageDialogImpl interface {
|
||||
@ -336,10 +347,12 @@ type SaveFileDialogOptions struct {
|
||||
AllowOtherFileTypes bool
|
||||
HideExtension bool
|
||||
TreatsFilePackagesAsDirectories bool
|
||||
Title string
|
||||
Message string
|
||||
Directory string
|
||||
Filename string
|
||||
ButtonText string
|
||||
Filters []FileFilter
|
||||
}
|
||||
|
||||
type SaveFileDialog struct {
|
||||
@ -354,10 +367,12 @@ type SaveFileDialog struct {
|
||||
directory string
|
||||
filename string
|
||||
buttonText string
|
||||
filters []FileFilter
|
||||
|
||||
window *WebviewWindow
|
||||
|
||||
impl saveFileDialogImpl
|
||||
impl saveFileDialogImpl
|
||||
title string
|
||||
}
|
||||
|
||||
type saveFileDialogImpl interface {
|
||||
@ -365,6 +380,7 @@ type saveFileDialogImpl interface {
|
||||
}
|
||||
|
||||
func (d *SaveFileDialog) SetOptions(options *SaveFileDialogOptions) {
|
||||
d.title = options.Title
|
||||
d.canCreateDirectories = options.CanCreateDirectories
|
||||
d.showHiddenFiles = options.ShowHiddenFiles
|
||||
d.canSelectHiddenExtension = options.CanSelectHiddenExtension
|
||||
@ -377,6 +393,16 @@ func (d *SaveFileDialog) SetOptions(options *SaveFileDialogOptions) {
|
||||
d.buttonText = options.ButtonText
|
||||
}
|
||||
|
||||
// AddFilter adds a filter to the dialog. The filter is a display name and a semicolon separated list of extensions.
|
||||
// EG: AddFilter("Image Files", "*.jpg;*.png")
|
||||
func (d *SaveFileDialog) AddFilter(displayName, pattern string) *SaveFileDialog {
|
||||
d.filters = append(d.filters, FileFilter{
|
||||
DisplayName: strings.TrimSpace(displayName),
|
||||
Pattern: strings.TrimSpace(pattern),
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialog) CanCreateDirectories(canCreateDirectories bool) *SaveFileDialog {
|
||||
d.canCreateDirectories = canCreateDirectories
|
||||
return d
|
||||
|
@ -364,8 +364,8 @@ func (m *macosDialog) show() {
|
||||
buttonPressed := int(C.dialogRunModal(m.nsDialog))
|
||||
if len(m.dialog.Buttons) > buttonPressed {
|
||||
button := reversedButtons[buttonPressed]
|
||||
if button.callback != nil {
|
||||
button.callback()
|
||||
if button.Callback != nil {
|
||||
button.Callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2,6 +2,14 @@
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/internal/go-common-file-dialog/cfd"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"golang.org/x/sys/windows"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (m *windowsApp) showAboutDialog(title string, message string, icon []byte) {
|
||||
panic("implement me")
|
||||
}
|
||||
@ -13,62 +21,26 @@ type windowsDialog struct {
|
||||
}
|
||||
|
||||
func (m *windowsDialog) show() {
|
||||
//
|
||||
//// Mac can only have 4 Buttons on a dialog
|
||||
//if len(m.dialog.Buttons) > 4 {
|
||||
// m.dialog.Buttons = m.dialog.Buttons[:4]
|
||||
//}
|
||||
//
|
||||
//if m.nsDialog != nil {
|
||||
// C.releaseDialog(m.nsDialog)
|
||||
//}
|
||||
//var title *C.char
|
||||
//if m.dialog.Title != "" {
|
||||
// title = C.CString(m.dialog.Title)
|
||||
//}
|
||||
//var message *C.char
|
||||
//if m.dialog.Message != "" {
|
||||
// message = C.CString(m.dialog.Message)
|
||||
//}
|
||||
//var iconData unsafe.Pointer
|
||||
//var iconLength C.int
|
||||
//if m.dialog.Icon != nil {
|
||||
// iconData = unsafe.Pointer(&m.dialog.Icon[0])
|
||||
// iconLength = C.int(len(m.dialog.Icon))
|
||||
//} else {
|
||||
// // if it's an error, use the application Icon
|
||||
// if m.dialog.DialogType == ErrorDialog {
|
||||
// iconData = unsafe.Pointer(&globalApplication.options.Icon[0])
|
||||
// iconLength = C.int(len(globalApplication.options.Icon))
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//alertType, ok := alertTypeMap[m.dialog.DialogType]
|
||||
//if !ok {
|
||||
// alertType = C.NSAlertStyleInformational
|
||||
//}
|
||||
//
|
||||
//m.nsDialog = C.createAlert(alertType, title, message, iconData, iconLength)
|
||||
//
|
||||
//// Reverse the Buttons so that the default is on the right
|
||||
//reversedButtons := make([]*Button, len(m.dialog.Buttons))
|
||||
//var count = 0
|
||||
//for i := len(m.dialog.Buttons) - 1; i >= 0; i-- {
|
||||
// button := m.dialog.Buttons[i]
|
||||
// C.alertAddButton(m.nsDialog, C.CString(button.Label), C.bool(button.IsDefault), C.bool(button.IsCancel))
|
||||
// reversedButtons[count] = m.dialog.Buttons[i]
|
||||
// count++
|
||||
//}
|
||||
//
|
||||
//buttonPressed := int(C.dialogRunModal(m.nsDialog))
|
||||
//if len(m.dialog.Buttons) > buttonPressed {
|
||||
// button := reversedButtons[buttonPressed]
|
||||
// if button.callback != nil {
|
||||
// button.callback()
|
||||
// }
|
||||
//}
|
||||
panic("implement me")
|
||||
|
||||
title := w32.MustStringToUTF16Ptr(m.dialog.Title)
|
||||
message := w32.MustStringToUTF16Ptr(m.dialog.Message)
|
||||
flags := calculateMessageDialogFlags(m.dialog.MessageDialogOptions)
|
||||
|
||||
button, _ := windows.MessageBox(windows.HWND(0), message, title, flags|windows.MB_SYSTEMMODAL)
|
||||
// This maps MessageBox return values to strings
|
||||
responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"}
|
||||
result := "Error"
|
||||
if int(button) < len(responses) {
|
||||
result = responses[button]
|
||||
}
|
||||
// Check if there's a callback for the button pressed
|
||||
for _, button := range m.dialog.Buttons {
|
||||
if button.Label == result {
|
||||
if button.Callback != nil {
|
||||
button.Callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *windowsDialog {
|
||||
@ -87,54 +59,49 @@ func newOpenFileDialogImpl(d *OpenFileDialog) *windowOpenFileDialog {
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultFolder(folder string) (string, error) {
|
||||
if folder == "" {
|
||||
return "", nil
|
||||
}
|
||||
return filepath.Abs(folder)
|
||||
}
|
||||
|
||||
func (m *windowOpenFileDialog) show() ([]string, error) {
|
||||
//openFileResponses[m.dialog.id] = make(chan string)
|
||||
//nsWindow := unsafe.Pointer(nil)
|
||||
//if m.dialog.window != nil {
|
||||
// // get NSWindow from window
|
||||
// nsWindow = m.dialog.window.impl.(*windowsWebviewWindow).nsWindow
|
||||
//}
|
||||
//
|
||||
//// Massage filter patterns into macOS format
|
||||
//// We iterate all filter patterns, tidy them up and then join them with a semicolon
|
||||
//// This should produce a single string of extensions like "png;jpg;gif"
|
||||
//var filterPatterns string
|
||||
//if len(m.dialog.filters) > 0 {
|
||||
// var allPatterns []string
|
||||
// for _, filter := range m.dialog.filters {
|
||||
// patternComponents := strings.Split(filter.Pattern, ";")
|
||||
// for i, component := range patternComponents {
|
||||
// filterPattern := strings.TrimSpace(component)
|
||||
// filterPattern = strings.TrimPrefix(filterPattern, "*.")
|
||||
// patternComponents[i] = filterPattern
|
||||
// }
|
||||
// allPatterns = append(allPatterns, strings.Join(patternComponents, ";"))
|
||||
// }
|
||||
// filterPatterns = strings.Join(allPatterns, ";")
|
||||
//}
|
||||
//
|
||||
//C.showOpenFileDialog(C.uint(m.dialog.id),
|
||||
// C.bool(m.dialog.canChooseFiles),
|
||||
// C.bool(m.dialog.canChooseDirectories),
|
||||
// C.bool(m.dialog.canCreateDirectories),
|
||||
// C.bool(m.dialog.showHiddenFiles),
|
||||
// C.bool(m.dialog.allowsMultipleSelection),
|
||||
// C.bool(m.dialog.resolvesAliases),
|
||||
// C.bool(m.dialog.hideExtension),
|
||||
// C.bool(m.dialog.treatsFilePackagesAsDirectories),
|
||||
// C.bool(m.dialog.allowsOtherFileTypes),
|
||||
// toCString(filterPatterns),
|
||||
// C.uint(len(filterPatterns)),
|
||||
// toCString(m.dialog.message),
|
||||
// toCString(m.dialog.directory),
|
||||
// toCString(m.dialog.buttonText),
|
||||
// nsWindow)
|
||||
//var result []string
|
||||
//for filename := range openFileResponses[m.dialog.id] {
|
||||
// result = append(result, filename)
|
||||
//}
|
||||
//return result, nil
|
||||
panic("implement me")
|
||||
|
||||
defaultFolder, err := getDefaultFolder(m.dialog.directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := cfd.DialogConfig{
|
||||
Title: m.dialog.title,
|
||||
Role: "PickFolder",
|
||||
FileFilters: convertFilters(m.dialog.filters),
|
||||
Folder: defaultFolder,
|
||||
}
|
||||
|
||||
var result []string
|
||||
if m.dialog.allowsMultipleSelection {
|
||||
temp, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewOpenMultipleFilesDialog(config)
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = temp.([]string)
|
||||
} else {
|
||||
temp, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewOpenFileDialog(config)
|
||||
}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = []string{temp.(string)}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type windowSaveFileDialog struct {
|
||||
@ -148,24 +115,71 @@ func newSaveFileDialogImpl(d *SaveFileDialog) *windowSaveFileDialog {
|
||||
}
|
||||
|
||||
func (m *windowSaveFileDialog) show() (string, error) {
|
||||
//saveFileResponses[m.dialog.id] = make(chan string)
|
||||
//nsWindow := unsafe.Pointer(nil)
|
||||
//if m.dialog.window != nil {
|
||||
// // get NSWindow from window
|
||||
// nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow
|
||||
//}
|
||||
//C.showSaveFileDialog(C.uint(m.dialog.id),
|
||||
// C.bool(m.dialog.canCreateDirectories),
|
||||
// C.bool(m.dialog.showHiddenFiles),
|
||||
// C.bool(m.dialog.canSelectHiddenExtension),
|
||||
// C.bool(m.dialog.hideExtension),
|
||||
// C.bool(m.dialog.treatsFilePackagesAsDirectories),
|
||||
// C.bool(m.dialog.allowOtherFileTypes),
|
||||
// toCString(m.dialog.message),
|
||||
// toCString(m.dialog.directory),
|
||||
// toCString(m.dialog.buttonText),
|
||||
// toCString(m.dialog.filename),
|
||||
// nsWindow)
|
||||
//return <-saveFileResponses[m.dialog.id], nil
|
||||
panic("implement me")
|
||||
defaultFolder, err := getDefaultFolder(m.dialog.directory)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config := cfd.DialogConfig{
|
||||
Title: m.dialog.title,
|
||||
Role: "SaveFile",
|
||||
FileFilters: convertFilters(m.dialog.filters),
|
||||
FileName: m.dialog.filename,
|
||||
Folder: defaultFolder,
|
||||
}
|
||||
|
||||
result, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewSaveFileDialog(config)
|
||||
}, false)
|
||||
return result.(string), nil
|
||||
}
|
||||
|
||||
func calculateMessageDialogFlags(options MessageDialogOptions) uint32 {
|
||||
var flags uint32
|
||||
|
||||
switch options.DialogType {
|
||||
case InfoDialog:
|
||||
flags = windows.MB_OK | windows.MB_ICONINFORMATION
|
||||
case ErrorDialog:
|
||||
flags = windows.MB_ICONERROR | windows.MB_OK
|
||||
case QuestionDialog:
|
||||
flags = windows.MB_YESNO
|
||||
for _, button := range options.Buttons {
|
||||
if strings.TrimSpace(strings.ToLower(button.Label)) == "no" && button.IsDefault {
|
||||
flags |= windows.MB_DEFBUTTON2
|
||||
}
|
||||
}
|
||||
case WarningDialog:
|
||||
flags = windows.MB_OK | windows.MB_ICONWARNING
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func convertFilters(filters []FileFilter) []cfd.FileFilter {
|
||||
var result []cfd.FileFilter
|
||||
for _, filter := range filters {
|
||||
result = append(result, cfd.FileFilter(filter))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool) (any, error) {
|
||||
dlg, err := newDlg()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
err := dlg.Release()
|
||||
if err != nil {
|
||||
println("ERROR: Unable to release dialog:", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
dlg.SetParentWindowHandle(0)
|
||||
if multi, _ := dlg.(cfd.OpenMultipleFilesDialog); multi != nil && isMultiSelect {
|
||||
return multi.ShowAndGetResults()
|
||||
}
|
||||
return dlg.ShowAndGetResult()
|
||||
}
|
||||
|
@ -3,32 +3,99 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type windowsMenuItem struct {
|
||||
menuItem *MenuItem
|
||||
|
||||
menuItemImpl unsafe.Pointer
|
||||
hMenu w32.HMENU
|
||||
id int
|
||||
label string
|
||||
disabled bool
|
||||
checked bool
|
||||
itemType menuItemType
|
||||
hidden bool
|
||||
submenu w32.HMENU
|
||||
}
|
||||
|
||||
func (m windowsMenuItem) setTooltip(tooltip string) {
|
||||
//C.setMenuItemTooltip(m.nsMenuItem, C.CString(tooltip))
|
||||
func (m *windowsMenuItem) setHidden(hidden bool) {
|
||||
m.hidden = hidden
|
||||
}
|
||||
|
||||
func (m windowsMenuItem) setLabel(s string) {
|
||||
//C.setMenuItemLabel(m.nsMenuItem, C.CString(s))
|
||||
func (m *windowsMenuItem) Checked() bool {
|
||||
return m.checked
|
||||
}
|
||||
|
||||
func (m windowsMenuItem) setDisabled(disabled bool) {
|
||||
//C.setMenuItemDisabled(m.nsMenuItem, C.bool(disabled))
|
||||
func (m *windowsMenuItem) IsSeparator() bool {
|
||||
return m.itemType == separator
|
||||
}
|
||||
|
||||
func (m windowsMenuItem) setChecked(checked bool) {
|
||||
//C.setMenuItemChecked(m.nsMenuItem, C.bool(checked))
|
||||
func (m *windowsMenuItem) IsCheckbox() bool {
|
||||
return m.itemType == checkbox
|
||||
}
|
||||
|
||||
func (m windowsMenuItem) setAccelerator(accelerator *accelerator) {
|
||||
func (m *windowsMenuItem) Enabled() bool {
|
||||
return !m.disabled
|
||||
}
|
||||
|
||||
func (m *windowsMenuItem) update() {
|
||||
var mii w32.MENUITEMINFO
|
||||
mii.CbSize = uint32(unsafe.Sizeof(mii))
|
||||
mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING
|
||||
if m.IsSeparator() {
|
||||
mii.FType = w32.MFT_SEPARATOR
|
||||
} else {
|
||||
mii.FType = w32.MFT_STRING
|
||||
//var text string
|
||||
//if s := a.shortcut; s.Key != 0 {
|
||||
// text = fmt.Sprintf("%s\t%s", a.text, s.String())
|
||||
// shortcut2Action[a.shortcut] = a
|
||||
//} else {
|
||||
// text = a.text
|
||||
//}
|
||||
mii.DwTypeData = w32.MustStringToUTF16Ptr(m.label)
|
||||
mii.Cch = uint32(len([]rune(m.label)))
|
||||
}
|
||||
mii.WID = uint32(m.id)
|
||||
if m.Enabled() {
|
||||
mii.FState &^= w32.MFS_DISABLED
|
||||
} else {
|
||||
mii.FState |= w32.MFS_DISABLED
|
||||
}
|
||||
|
||||
if m.IsCheckbox() {
|
||||
mii.FMask |= w32.MIIM_CHECKMARKS
|
||||
}
|
||||
if m.Checked() {
|
||||
mii.FState |= w32.MFS_CHECKED
|
||||
}
|
||||
|
||||
if m.menuItem.submenu != nil {
|
||||
mii.FMask |= w32.MIIM_SUBMENU
|
||||
mii.HSubMenu = m.submenu
|
||||
}
|
||||
|
||||
w32.SetMenuItemInfo(m.hMenu, uint32(m.id), false, &mii)
|
||||
}
|
||||
|
||||
func (m *windowsMenuItem) setLabel(label string) {
|
||||
m.label = label
|
||||
m.update()
|
||||
}
|
||||
|
||||
func (m *windowsMenuItem) setDisabled(disabled bool) {
|
||||
m.disabled = disabled
|
||||
m.update()
|
||||
}
|
||||
|
||||
func (m *windowsMenuItem) setChecked(checked bool) {
|
||||
m.checked = checked
|
||||
m.update()
|
||||
}
|
||||
|
||||
func (m *windowsMenuItem) setAccelerator(accelerator *accelerator) {
|
||||
//// Set the keyboard shortcut of the menu item
|
||||
//var modifier C.int
|
||||
//var key *C.char
|
||||
@ -41,23 +108,18 @@ func (m windowsMenuItem) setAccelerator(accelerator *accelerator) {
|
||||
//C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier)
|
||||
}
|
||||
|
||||
func newMenuItemImpl(item *MenuItem) *windowsMenuItem {
|
||||
func newMenuItemImpl(item *MenuItem, parentMenu w32.HMENU, ID int) *windowsMenuItem {
|
||||
result := &windowsMenuItem{
|
||||
menuItem: item,
|
||||
hMenu: parentMenu,
|
||||
id: ID,
|
||||
disabled: item.disabled,
|
||||
checked: item.checked,
|
||||
itemType: item.itemType,
|
||||
label: item.label,
|
||||
hidden: item.hidden,
|
||||
}
|
||||
//
|
||||
//switch item.itemType {
|
||||
//case text, checkbox, submenu, radio:
|
||||
// result.nsMenuItem = unsafe.Pointer(C.newMenuItem(C.uint(item.id), C.CString(item.label), C.bool(item.disabled), C.CString(item.tooltip)))
|
||||
// if item.itemType == checkbox || item.itemType == radio {
|
||||
// C.setMenuItemChecked(result.nsMenuItem, C.bool(item.checked))
|
||||
// }
|
||||
// if item.accelerator != nil {
|
||||
// result.setAccelerator(item.accelerator)
|
||||
// }
|
||||
//default:
|
||||
// panic("WTF")
|
||||
//}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -181,3 +243,9 @@ func newZoomMenuItem() *MenuItem {
|
||||
func newFullScreenMenuItem() *MenuItem {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// ---------- unsupported on windows ----------
|
||||
|
||||
func (m *windowsMenuItem) setTooltip(_ string) {
|
||||
// Unsupported
|
||||
}
|
||||
|
213
v3/pkg/application/popupmenu_windows.go
Normal file
213
v3/pkg/application/popupmenu_windows.go
Normal file
@ -0,0 +1,213 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
const (
|
||||
MenuItemMsgID = w32.WM_APP + 1024
|
||||
)
|
||||
|
||||
type RadioGroupMember struct {
|
||||
ID int
|
||||
MenuItem *MenuItem
|
||||
}
|
||||
|
||||
type RadioGroup []*RadioGroupMember
|
||||
|
||||
func (r *RadioGroup) Add(id int, item *MenuItem) {
|
||||
*r = append(*r, &RadioGroupMember{
|
||||
ID: id,
|
||||
MenuItem: item,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RadioGroup) Bounds() (int, int) {
|
||||
p := *r
|
||||
return p[0].ID, p[len(p)-1].ID
|
||||
}
|
||||
|
||||
func (r *RadioGroup) MenuID(item *MenuItem) int {
|
||||
for _, member := range *r {
|
||||
if member.MenuItem == item {
|
||||
return member.ID
|
||||
}
|
||||
}
|
||||
panic("RadioGroup.MenuID: item not found:")
|
||||
}
|
||||
|
||||
type PopupMenu struct {
|
||||
menu w32.PopupMenu
|
||||
parent w32.HWND
|
||||
menuMapping map[int]*MenuItem
|
||||
checkboxItems map[*MenuItem][]int
|
||||
radioGroups map[*MenuItem][]*RadioGroup
|
||||
menuData *Menu
|
||||
currentMenuID int
|
||||
onMenuClose func()
|
||||
onMenuOpen func()
|
||||
}
|
||||
|
||||
func (p *PopupMenu) buildMenu(parentMenu w32.PopupMenu, inputMenu *Menu) {
|
||||
var currentRadioGroup RadioGroup
|
||||
for _, item := range inputMenu.items {
|
||||
if item.Hidden() {
|
||||
continue
|
||||
}
|
||||
p.currentMenuID++
|
||||
itemID := p.currentMenuID
|
||||
p.menuMapping[itemID] = item
|
||||
|
||||
menuItemImpl := newMenuItemImpl(item, w32.HWND(parentMenu), itemID)
|
||||
|
||||
flags := uint32(w32.MF_STRING)
|
||||
if item.disabled {
|
||||
flags = flags | w32.MF_GRAYED
|
||||
}
|
||||
if item.checked && item.IsCheckbox() {
|
||||
flags = flags | w32.MF_CHECKED
|
||||
}
|
||||
if item.IsSeparator() {
|
||||
flags = flags | w32.MF_SEPARATOR
|
||||
}
|
||||
|
||||
if item.IsCheckbox() {
|
||||
p.checkboxItems[item] = append(p.checkboxItems[item], itemID)
|
||||
}
|
||||
if item.IsRadio() {
|
||||
currentRadioGroup.Add(itemID, item)
|
||||
} else {
|
||||
if len(currentRadioGroup) > 0 {
|
||||
for _, radioMember := range currentRadioGroup {
|
||||
currentRadioGroup := currentRadioGroup
|
||||
p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup)
|
||||
}
|
||||
currentRadioGroup = RadioGroup{}
|
||||
}
|
||||
}
|
||||
|
||||
if item.submenu != nil {
|
||||
flags = flags | w32.MF_POPUP
|
||||
newSubmenu := w32.CreatePopupMenu()
|
||||
p.buildMenu(newSubmenu, item.submenu)
|
||||
itemID = int(newSubmenu)
|
||||
menuItemImpl.submenu = w32.HWND(newSubmenu)
|
||||
}
|
||||
|
||||
var menuText = item.Label()
|
||||
|
||||
ok := parentMenu.Append(flags, uintptr(itemID), menuText)
|
||||
if !ok {
|
||||
w32.Fatal(fmt.Sprintf("Error adding menu item: %s", menuText))
|
||||
}
|
||||
|
||||
item.impl = menuItemImpl
|
||||
}
|
||||
if len(currentRadioGroup) > 0 {
|
||||
for _, radioMember := range currentRadioGroup {
|
||||
currentRadioGroup := currentRadioGroup
|
||||
p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup)
|
||||
}
|
||||
currentRadioGroup = RadioGroup{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopupMenu) Update() {
|
||||
p.menu = w32.CreatePopupMenu()
|
||||
p.menuMapping = make(map[int]*MenuItem)
|
||||
p.currentMenuID = MenuItemMsgID
|
||||
p.buildMenu(p.menu, p.menuData)
|
||||
p.updateRadioGroups()
|
||||
}
|
||||
|
||||
func NewPopupMenu(parent w32.HWND, inputMenu *Menu) *PopupMenu {
|
||||
result := &PopupMenu{
|
||||
parent: parent,
|
||||
menuData: inputMenu,
|
||||
checkboxItems: make(map[*MenuItem][]int),
|
||||
radioGroups: make(map[*MenuItem][]*RadioGroup),
|
||||
}
|
||||
result.Update()
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PopupMenu) ShowAtCursor() {
|
||||
x, y, ok := w32.GetCursorPos()
|
||||
if ok == false {
|
||||
w32.Fatal("GetCursorPos failed")
|
||||
}
|
||||
|
||||
w32.SetForegroundWindow(p.parent)
|
||||
|
||||
if p.onMenuOpen != nil {
|
||||
p.onMenuOpen()
|
||||
}
|
||||
|
||||
if p.menu.Track(p.parent, w32.TPM_LEFTALIGN, int32(x), int32(y-5)) == false {
|
||||
w32.Fatal("TrackPopupMenu failed")
|
||||
}
|
||||
|
||||
if p.onMenuClose != nil {
|
||||
p.onMenuClose()
|
||||
}
|
||||
|
||||
if !w32.PostMessage(p.parent, w32.WM_NULL, 0, 0) {
|
||||
w32.Fatal("PostMessage failed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *PopupMenu) ProcessCommand(cmdMsgID int) {
|
||||
item := p.menuMapping[cmdMsgID]
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
if item.IsRadio() {
|
||||
item.checked = true
|
||||
p.updateRadioGroup(item)
|
||||
}
|
||||
if item.callback != nil {
|
||||
item.handleClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopupMenu) Destroy() {
|
||||
p.menu.Destroy()
|
||||
}
|
||||
|
||||
func (p *PopupMenu) UpdateMenuItem(item *MenuItem) {
|
||||
if item.IsCheckbox() {
|
||||
for _, itemID := range p.checkboxItems[item] {
|
||||
p.menu.Check(uintptr(itemID), item.checked)
|
||||
}
|
||||
return
|
||||
}
|
||||
if item.IsRadio() && item.checked == true {
|
||||
p.updateRadioGroup(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopupMenu) updateRadioGroups() {
|
||||
for menuItem := range p.radioGroups {
|
||||
if menuItem.checked {
|
||||
p.updateRadioGroup(menuItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopupMenu) updateRadioGroup(item *MenuItem) {
|
||||
for _, radioGroup := range p.radioGroups[item] {
|
||||
thisMenuID := radioGroup.MenuID(item)
|
||||
startID, endID := radioGroup.Bounds()
|
||||
p.menu.CheckRadio(startID, endID, thisMenuID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopupMenu) OnMenuOpen(fn func()) {
|
||||
p.onMenuOpen = fn
|
||||
}
|
||||
|
||||
func (p *PopupMenu) OnMenuClose(fn func()) {
|
||||
p.onMenuClose = fn
|
||||
}
|
@ -38,6 +38,8 @@ type SystemTray struct {
|
||||
rightDoubleClickHandler func()
|
||||
mouseEnterHandler func()
|
||||
mouseLeaveHandler func()
|
||||
onMenuOpen func()
|
||||
onMenuClose func()
|
||||
|
||||
// Platform specific implementation
|
||||
impl systemTrayImpl
|
||||
|
@ -19,7 +19,7 @@ const (
|
||||
type windowsSystemTray struct {
|
||||
parent *SystemTray
|
||||
|
||||
menu *windowsMenu
|
||||
menu *PopupMenu
|
||||
|
||||
// Platform specific implementation
|
||||
uid uint32
|
||||
@ -30,8 +30,7 @@ type windowsSystemTray struct {
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) setMenu(menu *Menu) {
|
||||
//s.menu = menu
|
||||
panic("implement me")
|
||||
s.updateMenu(menu)
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) run() {
|
||||
@ -84,7 +83,7 @@ func (s *windowsSystemTray) run() {
|
||||
s.uid = nid.UID
|
||||
|
||||
if s.parent.menu != nil {
|
||||
s.updateMenu()
|
||||
s.updateMenu(s.parent.menu)
|
||||
}
|
||||
|
||||
// Set Default Callbacks
|
||||
@ -226,10 +225,11 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr
|
||||
return w32.DefWindowProc(s.hwnd, msg, wParam, lParam)
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) updateMenu() {
|
||||
s.menu = newMenuImpl(s.parent.menu)
|
||||
s.menu.hWnd = s.hwnd
|
||||
s.menu.update()
|
||||
func (s *windowsSystemTray) updateMenu(menu *Menu) {
|
||||
s.menu = NewPopupMenu(s.hwnd, menu)
|
||||
s.menu.onMenuOpen = s.parent.onMenuOpen
|
||||
s.menu.onMenuClose = s.parent.onMenuClose
|
||||
s.menu.Update()
|
||||
}
|
||||
|
||||
// ---- Unsupported ----
|
||||
|
@ -9,6 +9,9 @@ package w32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
@ -635,3 +638,11 @@ func WMMessageToString(msg uintptr) string {
|
||||
return fmt.Sprintf("0x%08x", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Fatal(message string) {
|
||||
println("***************** FATAL ERROR ******************")
|
||||
fmt.Println("Message: " + message)
|
||||
fmt.Println("Last Error: " + strconv.Itoa(int(GetLastError())))
|
||||
fmt.Println("Stack: " + string(debug.Stack()))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -117,7 +117,11 @@ func showWindow(hwnd uintptr, cmdshow int) bool {
|
||||
}
|
||||
|
||||
func MustStringToUTF16Ptr(input string) *uint16 {
|
||||
return lo.Must(syscall.UTF16PtrFromString(input))
|
||||
result, err := syscall.UTF16PtrFromString(input)
|
||||
if err != nil {
|
||||
Fatal(err.Error())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MustStringToUTF16uintptr(input string) uintptr {
|
||||
|
Loading…
Reference in New Issue
Block a user