[windows] Support Dialog API

This commit is contained in:
Lea Anthony 2021-06-19 16:29:49 +10:00
commit 102a8cc5a6
8 changed files with 274 additions and 18 deletions

View file

@ -7,6 +7,7 @@ require (
github.com/fatih/structtag v1.2.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gorilla/websocket v1.4.1
github.com/harry1453/go-common-file-dialog v1.0.0
github.com/imdario/mergo v0.3.11
github.com/jackmordaunt/icns v1.0.0
github.com/leaanthony/clir v1.0.4

View file

@ -10,6 +10,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@ -29,8 +31,12 @@ github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgj
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/harry1453/go-common-file-dialog v1.0.0 h1:fzBAGhRTqWQyJw5xkm0PSsA+d3CBYBrfh+Nayb6U0nM=
github.com/harry1453/go-common-file-dialog v1.0.0/go.mod h1:3zwmbo7fy+uYGyaec74mu+Z9DPg0aEt10fSjjPwfyiY=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ=

View file

@ -40,6 +40,9 @@ type Application struct {
// Logger
logger logger.CustomLogger
// Window handle (used by windows)
hwnd unsafe.Pointer
}
func (a *Application) saveMemoryReference(mem unsafe.Pointer) {

View file

@ -6,7 +6,9 @@ package ffenestri
import "C"
import (
"runtime"
"strconv"
"strings"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
@ -124,15 +126,45 @@ func (c *Client) WindowSetColour(colour int) {
// OpenDialog will open a dialog with the given title and filter
func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
filters := []string{}
if runtime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(dialogOptions.Filters),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(dialogOptions.AllowFiles),
c.app.bool2Cint(dialogOptions.AllowDirectories),
c.app.bool2Cint(dialogOptions.AllowMultiple),
c.app.bool2Cint(false),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.ResolvesAliases),
c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories),
)
}
// OpenMultipleDialog will open a dialog with the given title and filter
func (c *Client) OpenMultipleDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
filters := []string{}
if runtime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.OpenDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(dialogOptions.AllowFiles),
c.app.bool2Cint(dialogOptions.AllowDirectories),
c.app.bool2Cint(true),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),
c.app.bool2Cint(dialogOptions.CanCreateDirectories),
c.app.bool2Cint(dialogOptions.ResolvesAliases),
@ -142,10 +174,16 @@ func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string)
// SaveDialog will open a dialog with the given title and filter
func (c *Client) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
filters := []string{}
if runtime.GOOS == "darwin" {
for _, filter := range dialogOptions.Filters {
filters = append(filters, strings.Split(filter.Pattern, ",")...)
}
}
C.SaveDialog(c.app.app,
c.app.string2CString(callbackID),
c.app.string2CString(dialogOptions.Title),
c.app.string2CString(dialogOptions.Filters),
c.app.string2CString(strings.Join(filters, ";")),
c.app.string2CString(dialogOptions.DefaultFilename),
c.app.string2CString(dialogOptions.DefaultDirectory),
c.app.bool2Cint(dialogOptions.ShowHiddenFiles),

View file

@ -1,3 +1,5 @@
// +build !windows
package runtime
import (
@ -10,9 +12,11 @@ import (
// Dialog defines all Dialog related operations
type Dialog interface {
Open(dialogOptions *dialogoptions.OpenDialog) []string
Save(dialogOptions *dialogoptions.SaveDialog) string
Message(dialogOptions *dialogoptions.MessageDialog) string
OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error)
OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error)
OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error)
SaveFile(dialogOptions *dialogoptions.SaveDialog) (string, error)
Message(dialogOptions *dialogoptions.MessageDialog) (string, error)
}
// dialog exposes the Dialog interface
@ -44,8 +48,32 @@ func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
return title, filter
}
func OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error) {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:opendirectoryselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:selectdirectory:open:" + uniqueCallback
r.bus.Publish(message, dialogOptions)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string), nil
}
// Open prompts the user to select a file
func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string {
func (r *dialog) OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error) {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
@ -54,7 +82,7 @@ func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string {
responseTopic := "dialog:openselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:open:" + uniqueCallback
@ -66,11 +94,36 @@ func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string {
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().([]string)
return result.Data().(string), nil
}
// OpenMultiple prompts the user to select a file
func (r *dialog) OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error) {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:openmultipleselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:openmultiple:" + uniqueCallback
r.bus.Publish(message, dialogOptions)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string), nil
}
// Save prompts the user to select a file
func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string {
func (r *dialog) SaveFile(dialogOptions *dialogoptions.SaveDialog) (string, error) {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
@ -79,7 +132,7 @@ func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string {
responseTopic := "dialog:saveselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:save:" + uniqueCallback
@ -91,7 +144,7 @@ func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string {
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
return result.Data().(string), nil
}
// Message show a message to the user

View file

@ -0,0 +1,141 @@
// +build windows
package runtime
import (
"golang.org/x/sys/windows"
"syscall"
"github.com/harry1453/go-common-file-dialog/cfd"
"github.com/harry1453/go-common-file-dialog/cfdutil"
"github.com/wailsapp/wails/v2/internal/servicebus"
dialogoptions "github.com/wailsapp/wails/v2/pkg/options/dialog"
)
// Dialog defines all Dialog related operations
type Dialog interface {
OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error)
OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error)
OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error)
Save(dialogOptions *dialogoptions.SaveDialog) (string, error)
Message(dialogOptions *dialogoptions.MessageDialog) (string, error)
}
// dialog exposes the Dialog interface
type dialog struct {
bus *servicebus.ServiceBus
}
// newDialogs creates a new Dialogs struct
func newDialog(bus *servicebus.ServiceBus) Dialog {
return &dialog{
bus: bus,
}
}
// processTitleAndFilter return the title and filter from the given params.
// title is the first string, filter is the second
func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
var title, filter string
if len(params) > 0 {
title = params[0]
}
if len(params) > 1 {
filter = params[1]
}
return title, filter
}
func convertFilters(filters []dialogoptions.FileFilter) []cfd.FileFilter {
var result []cfd.FileFilter
for _, filter := range filters {
result = append(result, cfd.FileFilter(filter))
}
return result
}
func pickMultipleFiles(options *dialogoptions.OpenDialog) ([]string, error) {
results, err := cfdutil.ShowOpenMultipleFilesDialog(cfd.DialogConfig{
Title: options.Title,
Role: "OpenMultipleFiles",
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
Folder: options.DefaultDirectory,
})
return results, err
}
func (r *dialog) OpenMultipleFiles(options *dialogoptions.OpenDialog) ([]string, error) {
return pickMultipleFiles(options)
}
func (r *dialog) OpenDirectory(options *dialogoptions.OpenDialog) (string, error) {
return cfdutil.ShowPickFolderDialog(cfd.DialogConfig{
Title: options.Title,
Role: "PickFolder",
Folder: options.DefaultDirectory,
})
}
func (r *dialog) OpenFile(options *dialogoptions.OpenDialog) (string, error) {
result, err := cfdutil.ShowOpenFileDialog(cfd.DialogConfig{
Folder: options.DefaultDirectory,
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
})
return result, err
}
// Save prompts the user to select a file
func (r *dialog) Save(options *dialogoptions.SaveDialog) (string, error) {
result, err := cfdutil.ShowSaveFileDialog(cfd.DialogConfig{
Title: options.Title,
Role: "SaveFile",
FileName: options.DefaultFilename,
FileFilters: convertFilters(options.Filters),
})
return result, err
}
// Message show a message to the user
func (r *dialog) Message(options *dialogoptions.MessageDialog) (string, error) {
// TODO: error handling
title, err := syscall.UTF16PtrFromString(options.Title)
if err != nil {
return "", err
}
message, err := syscall.UTF16PtrFromString(options.Message)
if err != nil {
return "", err
}
var flags uint32
switch options.Type {
case dialogoptions.InfoDialog:
flags = windows.MB_OK | windows.MB_ICONINFORMATION
case dialogoptions.ErrorDialog:
flags = windows.MB_ICONERROR | windows.MB_OK
case dialogoptions.QuestionDialog:
flags = windows.MB_YESNO
case dialogoptions.WarningDialog:
flags = windows.MB_OK | windows.MB_ICONWARNING
}
result, _ := windows.MessageBox(0, message, title, flags|windows.MB_SYSTEMMODAL)
if options.Type == dialogoptions.QuestionDialog {
if result == 6 { // IDYES
return "Yes", nil
}
if result == 7 { // IDNO
return "No", nil
}
}
return "", nil
}

View file

@ -140,7 +140,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result := c.runtime.Dialog.Open(dialogOptions)
result, err := c.runtime.Dialog.OpenFile(dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
case "Dialog.Save":
dialogOptions := new(dialog.SaveDialog)
@ -148,7 +151,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result := c.runtime.Dialog.Save(dialogOptions)
result, err := c.runtime.Dialog.Save(dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
case "Dialog.Message":
dialogOptions := new(dialog.MessageDialog)
@ -156,7 +162,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
if err != nil {
c.logger.Error("Error decoding: %s", err)
}
result := c.runtime.Dialog.Message(dialogOptions)
result, err := c.runtime.Dialog.Message(dialogOptions)
if err != nil {
c.logger.Error("Error: %s", err)
}
c.sendResult(result, payload, clientID)
default:
c.logger.Error("Unknown system call: %+v", callName)

View file

@ -1,14 +1,19 @@
package dialog
// FileFilter defines a filter for dialog boxes
type FileFilter struct {
DisplayName string // Filter information EG: "Image Files (*.jpg, *.png)"
Pattern string // semi-colon separated list of extensions, EG: "*.jpg;*.png"
}
// OpenDialog contains the options for the OpenDialog runtime method
type OpenDialog struct {
DefaultDirectory string
DefaultFilename string
Title string
Filters string
Filters []FileFilter
AllowFiles bool
AllowDirectories bool
AllowMultiple bool
ShowHiddenFiles bool
CanCreateDirectories bool
ResolvesAliases bool
@ -20,7 +25,7 @@ type SaveDialog struct {
DefaultDirectory string
DefaultFilename string
Title string
Filters string
Filters []FileFilter
ShowHiddenFiles bool
CanCreateDirectories bool
TreatPackagesAsDirectories bool