5
0
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:
Lea Anthony 2023-05-12 20:56:07 +10:00
parent 2fbb21a84e
commit a23bb1e350
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
28 changed files with 1650 additions and 163 deletions

View File

@ -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.

View File

@ -20,6 +20,7 @@ func main() {
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)

View File

@ -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) {

View 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.

View 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

View 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
}

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -0,0 +1,7 @@
package cfd
import "errors"
var (
ErrorCancelled = errors.New("cancelled by user")
)

View 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())
}

View 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)
}

View 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
}

View 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))
}

View 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
}

View 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)
}

View 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()
}

View 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())
}

View 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())
}
}

View File

@ -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

View File

@ -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()
}
}
})

View File

@ -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()
}

View File

@ -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
}

View 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], &currentRadioGroup)
}
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], &currentRadioGroup)
}
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
}

View File

@ -38,6 +38,8 @@ type SystemTray struct {
rightDoubleClickHandler func()
mouseEnterHandler func()
mouseLeaveHandler func()
onMenuOpen func()
onMenuClose func()
// Platform specific implementation
impl systemTrayImpl

View File

@ -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 ----

View File

@ -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)
}

View File

@ -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 {