Skip to content

Commit 72f92d5

Browse files
committed
more fixes around DialogEx focus handling and default pushbuttons
I found some more areas inside existing walk code where it is trying to re-implement various dialog manager features that we want to avoid when using DialogEx: * Form's WM_ACTIVATE handler tries to save and restore the currently focused widget. This competes with the dialog manager, so I added an override in DialogEx to only track the active form. * I made WM_NEXTDLGCTL use SendMessage whenever possible by flagging when we're handling focus messages and only switching to PostMessage when we risk re-entering. We set that flag in WM_SETFOCUS and WM_KILLFOCUS handlers. * I prevent layout from forcibly focusing widgets when the enclosing form is a DialogEx; we want that dealt with elsewhere. * I created overrides for setCtrlID and clearCtrlID in PushButton that also set/clear BS_DEFPUSHBUTTON and re-focus the default pushbutton. This is necessary because setting focus to the default pushbutton typically happens when a dialog template is processed at creation time; of course, we're attaching widgets long after that has occurred. We also want to clear BS_DEFPUSHBUTTON once detaching widgets from the form, otherwise invalid state remains cached internally to Win32. Updates tailscale/corp#23789 Signed-off-by: Aaron Klotz <[email protected]>
1 parent 53d3395 commit 72f92d5

File tree

4 files changed

+105
-16
lines changed

4 files changed

+105
-16
lines changed

Diff for: dialogex.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,19 @@ func (dlg *DialogEx) dlgProc(hdlg win.HWND, msg uint32, wParam, lParam uintptr)
209209
case win.WM_NOTIFY:
210210
dlg.FormBase.WndProc(hdlg, msg, wParam, lParam)
211211
return true
212-
case win.WM_SETTINGCHANGE, win.WM_THEMECHANGED, win.WM_SETTEXT, win.WM_WINDOWPOSCHANGED, win.WM_SYSCOLORCHANGE, win.WM_DPICHANGED, win.WM_GETMINMAXINFO:
213-
dlg.FormBase.WndProc(hdlg, msg, wParam, lParam)
214-
// Once we handle these messages, we return false so that DefDlgProc also receives them.
212+
case win.WM_ACTIVATE:
213+
// FormBase's WM_ACTIVATE handler does some goofy stuff with focus that we
214+
// don't want, so we provide our own implementation.
215+
switch win.LOWORD(uint32(wParam)) {
216+
case win.WA_ACTIVE, win.WA_CLICKACTIVE:
217+
activeForm = dlg.AsFormBase()
218+
case win.WA_INACTIVE:
219+
activeForm = nil
220+
}
215221
return false
216222
default:
223+
// Once we handle these messages, we return false so that DefDlgProc also receives them.
224+
dlg.FormBase.WndProc(hdlg, msg, wParam, lParam)
217225
return false
218226
}
219227
}
@@ -251,13 +259,13 @@ func (dlg *DialogEx) Cancel() {
251259
// SetFocusNext moves keyboard focus to the next Widget in the dialog's tab
252260
// sequence.
253261
func (dlg *DialogEx) SetFocusNext() {
254-
win.PostMessage(dlg.hWnd, win.WM_NEXTDLGCTL, 0, 0)
262+
dlg.nextDlgCtl(0, 0)
255263
}
256264

257265
// SetFocusPrev moves keyboard focus to the previous Widget in the dialog's tab
258266
// sequence.
259267
func (dlg *DialogEx) SetFocusPrev() {
260-
win.PostMessage(dlg.hWnd, win.WM_NEXTDLGCTL, 1, 0)
268+
dlg.nextDlgCtl(1, 0)
261269
}
262270

263271
// SetFocusToWindow sets keyboard focus within dlg to the Window specified by w.
@@ -273,7 +281,7 @@ func (dlg *DialogEx) SetFocusToWindow(w Window) {
273281
return
274282
}
275283

276-
win.PostMessage(dlg.hWnd, win.WM_NEXTDLGCTL, uintptr(hwnd), 1)
284+
dlg.nextDlgCtl(uintptr(hwnd), 1)
277285
}
278286

279287
// EnterMode satisfies Modal.
@@ -422,3 +430,21 @@ func (dlg *DialogEx) routeWM_COMMAND(wParam, lParam uintptr) (result bool) {
422430
}
423431
return false
424432
}
433+
434+
// ResetDefaultFocus makes dlg re-determine its default pushbutton (if any),
435+
// and resets keyboard focus to dlg's default Widget.
436+
func (dlg *DialogEx) ResetDefaultFocus() {
437+
// Use the HWND variant, but with a null HWND.
438+
dlg.nextDlgCtl(0, 1)
439+
}
440+
441+
func (dlg *DialogEx) nextDlgCtl(wParam, lParam uintptr) {
442+
// Windows's keyboard focus-setting is not re-entrant. If we're already in
443+
// the midst of processing a focus change, we need to post this message
444+
// instead of sending it.
445+
if dlg.handlingFocusChange {
446+
win.PostMessage(dlg.hWnd, win.WM_NEXTDLGCTL, wParam, lParam)
447+
} else {
448+
win.SendMessage(dlg.hWnd, win.WM_NEXTDLGCTL, wParam, lParam)
449+
}
450+
}

Diff for: layout.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,10 @@ func applyLayoutResults(results []LayoutResult, stopwatch *stopwatch) error {
339339
activeForm, _ := windowFromHandle(hwndForm).(Form)
340340

341341
if hwndFocused == 0 || form.Handle() == hwndFocused || activeForm != window.Form() {
342-
form.AsFormBase().clientComposite.focusFirstCandidateDescendant()
342+
// Only set explicit focus if we're not a DialogEx.
343+
if _, isDialogEx := form.(DialogExResolver); !isDialogEx {
344+
form.AsFormBase().clientComposite.focusFirstCandidateDescendant()
345+
}
343346
}
344347
}()
345348
}

Diff for: pushbutton.go

+63-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type PushButton struct {
1717
Button
1818
contentMargins win.MARGINS
1919
layoutFlags LayoutFlags
20+
wantDefault bool
2021
}
2122

2223
// NewPushButton creates a new PushButton as a child of parent with its
@@ -30,7 +31,7 @@ func NewPushButton(parent Container) (*PushButton, error) {
3031
type PushButtonOptions struct {
3132
LayoutFlags LayoutFlags // LayoutFlags to be used by the PushButton.
3233
PredefinedID int // When non-zero, must be one of the predefined control IDs <= [win.IDCONTINUE].
33-
Default bool // When true, the PushButton will be initially created as a default PushButton.
34+
Default bool // When true, the PushButton will set itself as the default PushButton for the Form it resides in.
3435
}
3536

3637
// NewPushButtonWithOptions creates a new PushButton as a child of parent
@@ -42,20 +43,14 @@ func NewPushButtonWithOptions(parent Container, opts PushButtonOptions) (*PushBu
4243

4344
pb := &PushButton{
4445
layoutFlags: opts.LayoutFlags,
45-
}
46-
47-
style := uint32(win.WS_TABSTOP | win.WS_VISIBLE)
48-
if opts.Default {
49-
style |= win.BS_DEFPUSHBUTTON
50-
} else {
51-
style |= win.BS_PUSHBUTTON
46+
wantDefault: opts.Default,
5247
}
5348

5449
if err := InitWidget(
5550
pb,
5651
parent,
5752
"BUTTON",
58-
style,
53+
win.WS_TABSTOP|win.WS_VISIBLE,
5954
0); err != nil {
6055
return nil, err
6156
}
@@ -86,6 +81,65 @@ func (pb *PushButton) SetImageAboveText(value bool) error {
8681
return pb.SetImage(pb.image)
8782
}
8883

84+
func (pb *PushButton) isDefault() bool {
85+
return pb.hasStyleBits(win.BS_DEFPUSHBUTTON)
86+
}
87+
88+
func (pb *PushButton) setCtrlID(ids ctrlIDAllocator) {
89+
var id uint16
90+
if !pb.usesPredefinedID {
91+
id = ids.allocCtrlID()
92+
pb.setCtrlIDInternal(id)
93+
}
94+
95+
if pb.wantDefault {
96+
if dlgExResolver, ok := pb.ancestor().(DialogExResolver); ok {
97+
if pb.usesPredefinedID {
98+
// We need to know the existing predefined ID.
99+
id = pb.getCtrlID()
100+
}
101+
102+
// Ensure BS_DEFPUSHBUTTON is set.
103+
pb.setAndClearStyleBits(win.BS_DEFPUSHBUTTON, win.BS_PUSHBUTTON)
104+
105+
dlgEx := dlgExResolver.AsDialogEx()
106+
// IDs are being assigned by FormBase code after the DialogEx has already
107+
// been created, so we need to inform the dialog that our control ID
108+
// represents the default button.
109+
win.SendMessage(dlgEx.hWnd, win.DM_SETDEFID, uintptr(id), 0)
110+
dlgEx.SetFocusToWindow(pb)
111+
}
112+
}
113+
}
114+
115+
func (pb *PushButton) clearCtrlID(ids ctrlIDAllocator) {
116+
var id uint16
117+
if !pb.usesPredefinedID {
118+
id = pb.setCtrlIDInternal(0)
119+
ids.freeCtrlID(id)
120+
}
121+
122+
if pb.wantDefault {
123+
if dlgExResolver, ok := pb.ancestor().(DialogExResolver); ok {
124+
if pb.usesPredefinedID {
125+
// We need to know the existing predefined ID.
126+
id = pb.getCtrlID()
127+
}
128+
129+
// Ensure BS_DEFPUSHBUTTON is cleared.
130+
pb.setAndClearStyleBits(win.BS_PUSHBUTTON, win.BS_DEFPUSHBUTTON)
131+
132+
dlgEx := dlgExResolver.AsDialogEx()
133+
// See whether the dialog's current default ID is ours...
134+
result := uint32(win.SendMessage(dlgEx.hWnd, win.DM_GETDEFID, 0, 0))
135+
if win.HIWORD(result)&win.DC_HASDEFID != 0 && win.LOWORD(result) == id {
136+
// ...and if so, clear it.
137+
win.SendMessage(dlgEx.hWnd, win.DM_SETDEFID, uintptr(0), 0)
138+
}
139+
}
140+
}
141+
}
142+
89143
func (pb *PushButton) ensureProperDialogDefaultButton(hwndFocus win.HWND) {
90144
widget := windowFromHandle(hwndFocus)
91145
if widget == nil {

Diff for: window.go

+6
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ type WindowBase struct {
455455
calcTextSizeInfo2TextSize map[calcTextSizeInfo]Size // in native pixels
456456
suspended bool
457457
enabled bool
458+
handlingFocusChange bool
458459
acc *Accessibility
459460
themes map[string]*Theme
460461
menuSharedMetricsInitialDPI *menuSharedMetrics
@@ -2550,6 +2551,11 @@ func (wb *WindowBase) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr)
25502551
wb.publishMouseWheelEvent(&wb.mouseWheelPublisher, wParam, lParam)
25512552

25522553
case win.WM_SETFOCUS, win.WM_KILLFOCUS:
2554+
wb.handlingFocusChange = true
2555+
defer func() {
2556+
wb.handlingFocusChange = false
2557+
}()
2558+
25532559
switch wnd := wb.window.(type) {
25542560
// case *splitterHandle:
25552561
// nop

0 commit comments

Comments
 (0)