Skip to content

Commit 8865133

Browse files
committed
add Win32Window, DialogEx, Monitor, and fix control ID allocation
I refactored some common (but very basic) HWND operations into Win32Window. One reason I did this was to allow the default WndProc to be overridden. I added two new types of PreTranslateHandler managed by the Application. Global PreTranslateHandlers are always run first, regardless of the HWND. If no global handlers processed the message, we then run HWND-specific PreTranslateHandlers, which are associated with a specific HWND and may be added and removed as necessary. If the message is still unprocessed, we then fall through to the existing implementation. Global handlers are not used yet (they are needed for WinUI), but since I was already adding HWND-specific handlers, I put them in anyway. I added Monitor, an abstraction for various HMONITOR operations. I decided that some of the walk stuff around dialog management was too fargone to fix without breaking a lot of existing code. Instead I wrote a new component called DialogEx. It essentially creates a dialog using the Win32 dialog manager (via CreateDialogIndirectParam) but is still able to act as a Form and work with walk's layout code. We use a custom window class for this stuff essentially to indicate to the system that the dialog is nonstandard. The point of DialogEx is to leverage the existing dialog management functionality instead of pushing back on it or trying to reimplement it. Note that because DialogEx implements a dialog procedure, we need to override FormBase's default WndProc with a no-op (courtesy of the Win32Window refactoring). I changed how keyboard focus is set in Form so that DialogEx can override. I updated PushButton and ContainerBase such that, when running within a DialogEx, they skip some of their walk-specific message processing. This skips a bunch of code that is essentially trying to do a bad job of reimplementing dialog management. I updated PushButton to allow reserved control IDs to be explicitly set. We also update PushButton to allow itself to be declaratively set as the default pushbutton within a DialogEx. I removed control ID allocation from ContainerBase. Instead, Widgets request an IDAllocator from their owning FormBase, ensuring uniqueness across all Widgets in the Form's widget hierarchy, not just the ones residing within a specific container. I also updated go.mod to pick up the latest win. Fixes #96 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
1 parent 2078ab9 commit 8865133

16 files changed

Lines changed: 956 additions & 170 deletions

action.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ type Action struct {
6161

6262
// aaron: I don't know why Walk uses menu item IDs for IDOK and IDCANCEL, but
6363
// for now we need to reserve IDs up to and including IDCANCEL (2).
64-
const maxReservedID = win.IDCANCEL
64+
const maxReservedActionID = win.IDCANCEL
6565

6666
func makeIDAllocator() idalloc.IDAllocator {
6767
alloc := idalloc.New(1 << 16)
68-
for i := 0; i <= maxReservedID; i++ {
68+
for i := 0; i <= maxReservedActionID; i++ {
6969
alloc.Allocate()
7070
}
7171
return alloc
@@ -80,7 +80,7 @@ func allocActionID() uint16 {
8080
}
8181

8282
func freeActionID(id uint16) {
83-
if id <= maxReservedID {
83+
if id <= maxReservedActionID {
8484
return
8585
}
8686
actionIDs.Free(uint32(id))
@@ -137,7 +137,7 @@ func (a *Action) finalize() {
137137

138138
func (a *Action) addRef() {
139139
a.refCount++
140-
if a.refCount == 1 && a.id > maxReservedID {
140+
if a.refCount == 1 && a.id > maxReservedActionID {
141141
actionsById[a.id] = a
142142
if sc := a.shortcut; sc.Key != 0 {
143143
shortcut2Action[sc] = a
@@ -147,7 +147,7 @@ func (a *Action) addRef() {
147147

148148
func (a *Action) release() {
149149
a.refCount--
150-
if a.refCount == 0 && a.id > maxReservedID {
150+
if a.refCount == 0 && a.id > maxReservedActionID {
151151
delete(actionsById, a.id)
152152
if sc := a.shortcut; sc.Key != 0 && shortcut2Action[sc] == a {
153153
delete(shortcut2Action, sc)

application.go

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"errors"
1313
"fmt"
1414
"log"
15+
"os"
1516
"runtime"
1617
"sync"
1718
"sync/atomic"
@@ -104,29 +105,31 @@ func (o *onceWithPreInit) doPreInit(f func()) bool {
104105
// of the application. There is only one singleton instance. Use InitApp to
105106
// initialize it, and then use App for the duration of the process to access it.
106107
type Application struct {
107-
uiThreadID uint32
108-
ctx context.Context
109-
ctxCancel context.CancelFunc
110-
walkInit []func()
111-
organizationName atomic.Pointer[string]
112-
productName atomic.Pointer[string]
113-
settings atomic.Value // of Settings
114-
exiting atomic.Bool
115-
panickingPublisher ErrorEventPublisher
116-
nextMsg uint32
117-
syncFuncMsg uint32
118-
syncLayoutMsg uint32
119-
cloakChangeMsg uint32
120-
winEventProc uintptr
121-
winEventHook win.HWINEVENTHOOK
122-
msgWindow win.HWND
123-
syncFuncsMutex sync.Mutex
124-
syncFuncs []func()
125-
syncLayoutMutex sync.Mutex
126-
layoutResultsByForm map[Form]*formLayoutResult // Layout computations queued for application
127-
pToolTip *ToolTip
128-
activeMessageLoops int
129-
runMsgFilters bool
108+
uiThreadID uint32
109+
ctx context.Context
110+
ctxCancel context.CancelFunc
111+
walkInit []func()
112+
organizationName atomic.Pointer[string]
113+
productName atomic.Pointer[string]
114+
settings atomic.Value // of Settings
115+
exiting atomic.Bool
116+
panickingPublisher ErrorEventPublisher
117+
nextMsg uint32
118+
syncFuncMsg uint32
119+
syncLayoutMsg uint32
120+
cloakChangeMsg uint32
121+
winEventProc uintptr
122+
winEventHook win.HWINEVENTHOOK
123+
msgWindow win.HWND
124+
syncFuncsMutex sync.Mutex
125+
syncFuncs []func()
126+
syncLayoutMutex sync.Mutex
127+
layoutResultsByForm map[Form]*formLayoutResult // Layout computations queued for application
128+
pToolTip *ToolTip
129+
globalPreTranslateHandlers []PreTranslateHandler
130+
perWindowPreTranslateHandlers map[win.HWND]PreTranslateHandler
131+
activeMessageLoops int
132+
runMsgFilters bool
130133
}
131134

132135
// Bare minimum initialization that must happen ASAP. While we typically do
@@ -254,8 +257,8 @@ func (app *Application) Panicking() *ErrorEvent {
254257
}
255258

256259
// maybePublishPanic is used by walk's top-level WndProcs to recover any
257-
// panics that occurred further down the call stack, convert them to errors,
258-
// and publish them as an event.
260+
// panic that occurred further down the call stack, convert it to an error,
261+
// and publish it as an event.
259262
func (app *Application) maybePublishPanic() {
260263
if len(app.panickingPublisher.event.handlers) == 0 {
261264
return
@@ -353,6 +356,7 @@ func (app *Application) init() (finalInitOutsideOnce func() error, err error) {
353356
}
354357

355358
app.layoutResultsByForm = make(map[Form]*formLayoutResult)
359+
app.perWindowPreTranslateHandlers = make(map[win.HWND]PreTranslateHandler)
356360
defaultWndProcPtr = windows.NewCallback(defaultWndProc)
357361

358362
walkInits := app.walkInit
@@ -429,6 +433,21 @@ func (app *Application) runMainMessageLoop() int {
429433
}
430434

431435
func (app *Application) runPreTranslateHandler(msg *win.MSG) bool {
436+
// Order is important here: run the global handlers first...
437+
for _, handler := range app.globalPreTranslateHandlers {
438+
if handler.OnPreTranslate(msg) {
439+
return true
440+
}
441+
}
442+
443+
// ...Then the per-window handlers...
444+
for _, handler := range app.perWindowPreTranslateHandlers {
445+
if handler.OnPreTranslate(msg) {
446+
return true
447+
}
448+
}
449+
450+
// ...Then, if present, the handler associated with msg's HWND.
432451
w := getMsgWindow(msg)
433452
if w == nil {
434453
return false
@@ -791,3 +810,33 @@ func (app *Application) EnableMessageFilterHooks(enable bool) {
791810
func (app *Application) Context() context.Context {
792811
return app.ctx
793812
}
813+
814+
// AddGlobalPreTranslateHandler registers handler to be unconditionally run at
815+
// each iteration of the main event loop. Once added, handler remains registered
816+
// for the remaining life of the process. This method must be called from the
817+
// main goroutine.
818+
func (app *Application) AddGlobalPreTranslateHandler(handler PreTranslateHandler) {
819+
app.AssertUIThread()
820+
if handler != nil {
821+
app.globalPreTranslateHandlers = append(app.globalPreTranslateHandlers, handler)
822+
}
823+
}
824+
825+
// AddPreTranslateHandlerForHWND registers handler keyed by hwnd, to be run
826+
// at each iteration of the main event loop. This method must be called from
827+
// the main goroutine.
828+
func (app *Application) AddPreTranslateHandlerForHWND(hwnd win.HWND, handler PreTranslateHandler) {
829+
app.AssertUIThread()
830+
if hwnd == 0 || handler == nil {
831+
panic(os.ErrInvalid)
832+
}
833+
app.perWindowPreTranslateHandlers[hwnd] = handler
834+
}
835+
836+
// DeletePreTranslateHandlerForHWND removes any handler keyed by hwnd that
837+
// was previously registered by [Application.AddPreTranslateHandlerForHWND]. This method must
838+
// be called from the main goroutine.
839+
func (app *Application) DeletePreTranslateHandlerForHWND(hwnd win.HWND) {
840+
app.AssertUIThread()
841+
delete(app.perWindowPreTranslateHandlers, hwnd)
842+
}

container.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ type Container interface {
2727

2828
type ContainerBase struct {
2929
WidgetBase
30-
layout Layout
31-
children *WidgetList
32-
dataBinder *DataBinder
33-
nextChildID int32
34-
persistent bool
30+
layout Layout
31+
children *WidgetList
32+
dataBinder *DataBinder
33+
persistent bool
3534
}
3635

3736
func (cb *ContainerBase) AsWidgetBase() *WidgetBase {
@@ -42,11 +41,6 @@ func (cb *ContainerBase) AsContainerBase() *ContainerBase {
4241
return cb
4342
}
4443

45-
func (cb *ContainerBase) NextChildID() int32 {
46-
cb.nextChildID++
47-
return cb.nextChildID
48-
}
49-
5044
func (cb *ContainerBase) applyEnabled(enabled bool) {
5145
cb.WidgetBase.applyEnabled(enabled)
5246

@@ -301,6 +295,11 @@ func (cb *ContainerBase) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintp
301295
return 0
302296

303297
case win.WM_COMMAND:
298+
if dlgExResolver, ok := ancestor(cb).(DialogExResolver); ok {
299+
// Let the DialogEx handle this.
300+
dlgExResolver.AsDialogEx().SendMessage(win.WM_COMMAND, wParam, lParam)
301+
return 0
302+
}
304303
if lParam == 0 {
305304
switch win.HIWORD(uint32(wParam)) {
306305
case 0:

declarative/dialogex.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
//go:build windows
5+
// +build windows
6+
7+
package declarative
8+
9+
import (
10+
"github.com/tailscale/walk"
11+
)
12+
13+
type DialogEx struct {
14+
Background Brush
15+
Layout Layout
16+
Children []Widget
17+
Icon Property
18+
Title string
19+
Size Size
20+
21+
AssignTo **walk.DialogEx
22+
}
23+
24+
func (d DialogEx) Create(owner walk.Form) error {
25+
dlg, err := walk.NewDialogEx(owner, d.Title, d.Size.toW())
26+
if err != nil {
27+
return err
28+
}
29+
30+
if d.AssignTo != nil {
31+
*d.AssignTo = dlg
32+
}
33+
34+
fi := formInfo{
35+
// Window
36+
Background: d.Background,
37+
Enabled: true,
38+
39+
// Container
40+
Children: d.Children,
41+
Layout: d.Layout,
42+
43+
// Form
44+
Icon: d.Icon,
45+
Title: d.Title,
46+
}
47+
48+
builder := NewBuilder(nil)
49+
dlg.SetSuspended(true)
50+
builder.Defer(func() error {
51+
dlg.SetSuspended(false)
52+
return nil
53+
})
54+
55+
return builder.InitWidget(fi, dlg, nil)
56+
}

declarative/pushbutton.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,22 @@ type PushButton struct {
5858
// PushButton
5959

6060
AssignTo **walk.PushButton
61+
PredefinedID int
6162
ImageAboveText bool
63+
Default bool
6264
}
6365

6466
func (pb PushButton) Create(builder *Builder) (err error) {
65-
var w *walk.PushButton
66-
if pb.UseLayoutFlags {
67-
w, err = walk.NewPushButtonWithLayoutFlags(builder.Parent(), pb.LayoutFlags)
68-
} else {
69-
w, err = walk.NewPushButton(builder.Parent())
67+
opts := walk.PushButtonOptions{
68+
LayoutFlags: pb.LayoutFlags,
69+
Default: pb.Default,
70+
PredefinedID: pb.PredefinedID,
7071
}
72+
if !pb.UseLayoutFlags {
73+
opts.LayoutFlags = walk.GrowableHorz
74+
}
75+
76+
w, err := walk.NewPushButtonWithOptions(builder.Parent(), opts)
7177
if err != nil {
7278
return err
7379
}

0 commit comments

Comments
 (0)