Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions v3/pkg/application/dialogs_taskdialog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package application

import (
"sync"
"testing"
)

func TestTaskDialogButtonCallbackStorage(t *testing.T) {
callbacks := make(map[int32]func())
var mu sync.Mutex

quitCalled := false
cancelCalled := false

callbacks[100] = func() { quitCalled = true }
callbacks[101] = func() { cancelCalled = true }

mu.Lock()
if cb, ok := callbacks[100]; ok && cb != nil {
cb()
}
mu.Unlock()

if !quitCalled {
t.Error("callback for button 100 should have been called")
}
if cancelCalled {
t.Error("callback for button 101 should NOT have been called yet")
}

mu.Lock()
if cb, ok := callbacks[101]; ok && cb != nil {
cb()
}
mu.Unlock()

if !cancelCalled {
t.Error("callback for button 101 should have been called")
}
}

func TestTaskDialogButtonIDMapping(t *testing.T) {
buttons := []*Button{
{Label: "Quit", IsDefault: true},
{Label: "Cancel", IsCancel: true},
{Label: "Retry"},
}

const customButtonBase = 100
for i, btn := range buttons {
id := int32(customButtonBase + i)
expectedID := int32(100 + i)
if id != expectedID {
t.Errorf("button %q: id = %d, want %d", btn.Label, id, expectedID)
}
if btn.Label == "Quit" && !btn.IsDefault {
t.Error("Quit button should be default")
}
if btn.Label == "Cancel" && !btn.IsCancel {
t.Error("Cancel button should be cancel")
}
}
}

func TestTaskDialogDefaultButtonSelection(t *testing.T) {
buttons := []*Button{
{Label: "No", IsDefault: false},
{Label: "Yes", IsDefault: true},
}

var defaultButtonID int32
const customButtonBase = 100
for i, btn := range buttons {
if btn.IsDefault {
defaultButtonID = int32(customButtonBase + i)
}
}

if defaultButtonID != 101 {
t.Errorf("defaultButtonID = %d, want 101", defaultButtonID)
}
}
6 changes: 3 additions & 3 deletions v3/pkg/application/dialogs_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type windowsDialog struct {
}

func (m *windowsDialog) show() {
if len(m.dialog.Buttons) > 0 && showTaskDialog(m.dialog) {
return
}

title := w32.MustStringToUTF16Ptr(m.dialog.Title)
message := w32.MustStringToUTF16Ptr(m.dialog.Message)
Expand All @@ -47,7 +50,6 @@ func (m *windowsDialog) show() {
}

if m.UseAppIcon || m.dialog.Icon != nil {
// 3 is the application icon
button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON)
if err != nil {
globalApplication.handleFatalError(err)
Expand All @@ -58,13 +60,11 @@ func (m *windowsDialog) show() {
globalApplication.handleFatalError(err)
}
}
// 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 _, buttonInDialog := range m.dialog.Buttons {
if buttonInDialog.Label == result {
if buttonInDialog.Callback != nil {
Expand Down
171 changes: 171 additions & 0 deletions v3/pkg/application/dialogs_windows_taskdialog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//go:build windows

package application

import (
"sync"
"syscall"
"unsafe"
)

const (
tdfAllowDialogCancellation = 0x0008
)

const (
tdcbfOkButton = 0x0001
tdcbfCancelButton = 0x0008
)

const (
tdiInformationIcon = 65534
tdiErrorIcon = 65531
tdiWarningIcon = 65533
)

type taskDialogButton struct {
nButtonID int32
pszButtonText *uint16
}

type taskDialogConfig struct {
cbSize uint32
hwndParent uintptr
hInstance uintptr
dwFlags uint32
dwCommonButtons uint32
pszWindowTitle *uint16
_ uintptr
pszMainIcon *uint16
pszMainInstruction *uint16
pszContent *uint16
cButtons uint32
pButtons uintptr
nDefaultButton int32
cRadioButtons uint32
pRadioButtons uintptr
nDefaultRadioButton int32
pszVerificationText *uint16
pszExpandedInfo *uint16
pszExpandedCtrlText *uint16
pszCollapsedCtrlText *uint16
_ uintptr
pszFooterIcon *uint16
pszFooter *uint16
pfCallback uintptr
lpCallbackData uintptr
cxWidth uint32
}

var (
lazyComctl32 = syscall.NewLazyDLL("comctl32.dll")
procTaskDialogIndirect = lazyComctl32.NewProc("TaskDialogIndirect")
taskDialogCallbackMutex sync.Mutex
taskDialogButtonCallback map[int32]func()
)

func init() {
taskDialogButtonCallback = make(map[int32]func())
}

func taskDialogAvailable() bool {
return procTaskDialogIndirect.Find() == nil
}

func showTaskDialog(dialog *MessageDialog) bool {
if !taskDialogAvailable() {
return false
}

var parentWindow uintptr
if dialog.window != nil {
if nativeWindow := dialog.window.NativeWindow(); nativeWindow != nil {
parentWindow = uintptr(nativeWindow)
}
}

cfg := taskDialogConfig{
cbSize: uint32(unsafe.Sizeof(taskDialogConfig{})),
hwndParent: parentWindow,
dwFlags: tdfAllowDialogCancellation,
}

if dialog.Title != "" {
cfg.pszWindowTitle = syscall.StringToUTF16Ptr(dialog.Title)
}

if dialog.Message != "" {
cfg.pszMainInstruction = syscall.StringToUTF16Ptr(dialog.Message)
}

switch dialog.DialogType {
case InfoDialogType:
cfg.pszMainIcon = makeIntResource(tdiInformationIcon)
case ErrorDialogType:
cfg.pszMainIcon = makeIntResource(tdiErrorIcon)
case WarningDialogType:
cfg.pszMainIcon = makeIntResource(tdiWarningIcon)
case QuestionDialogType:
cfg.pszMainIcon = makeIntResource(tdiInformationIcon)
}

if len(dialog.Buttons) == 0 {
cfg.dwCommonButtons = tdcbfOkButton
}

var buttons []taskDialogButton
const customButtonBase = 100

taskDialogCallbackMutex.Lock()
for id := range taskDialogButtonCallback {
delete(taskDialogButtonCallback, id)
}

for i, btn := range dialog.Buttons {
id := int32(customButtonBase + i)
buttons = append(buttons, taskDialogButton{
nButtonID: id,
pszButtonText: syscall.StringToUTF16Ptr(btn.Label),
})
if btn.Callback != nil {
taskDialogButtonCallback[id] = btn.Callback
}
if btn.IsDefault {
cfg.nDefaultButton = id
}
}
taskDialogCallbackMutex.Unlock()

if len(buttons) > 0 {
cfg.cButtons = uint32(len(buttons))
cfg.pButtons = uintptr(unsafe.Pointer(&buttons[0]))
}

var buttonPressed int32
ret, _, _ := procTaskDialogIndirect.Call(
uintptr(unsafe.Pointer(&cfg)),
uintptr(unsafe.Pointer(&buttonPressed)),
0,
0,
)

if ret != 0 {
return false
}

if buttonPressed >= customButtonBase {
taskDialogCallbackMutex.Lock()
if cb, ok := taskDialogButtonCallback[buttonPressed]; ok && cb != nil {
taskDialogCallbackMutex.Unlock()
cb()
return true
}
taskDialogCallbackMutex.Unlock()
}

return true
}

func makeIntResource(id uint16) *uint16 {
return (*uint16)(unsafe.Pointer(uintptr(id)))
}
Loading