// Copyright 2012 The Walk Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build windows package walk import ( "fmt" "math" "sync" "syscall" "time" "unsafe" "github.com/lxn/win" ) type CloseReason byte const ( CloseReasonUnknown CloseReason = iota CloseReasonUser ) var ( syncFuncs struct { m sync.Mutex funcs []func() } syncMsgId uint32 taskbarButtonCreatedMsgId uint32 ) func init() { AppendToWalkInit(func() { syncMsgId = win.RegisterWindowMessage(syscall.StringToUTF16Ptr("WalkSync")) taskbarButtonCreatedMsgId = win.RegisterWindowMessage(syscall.StringToUTF16Ptr("TaskbarButtonCreated")) }) } type Form interface { Container AsFormBase() *FormBase Run() int Starting() *Event Closing() *CloseEvent Activating() *Event Deactivating() *Event Activate() error Show() Hide() Title() string SetTitle(title string) error TitleChanged() *Event Icon() Image SetIcon(icon Image) error IconChanged() *Event Owner() Form SetOwner(owner Form) error ProgressIndicator() *ProgressIndicator // RightToLeftLayout returns whether coordinates on the x axis of the // Form increase from right to left. RightToLeftLayout() bool // SetRightToLeftLayout sets whether coordinates on the x axis of the // Form increase from right to left. SetRightToLeftLayout(rtl bool) error } type FormBase struct { WindowBase clientComposite *Composite owner Form stopwatch *stopwatch inProgressEventCount int performLayout chan ContainerLayoutItem layoutResults chan []LayoutResult inSizeLoop chan bool updateStopwatch chan *stopwatch quitLayoutPerformer chan struct{} closingPublisher CloseEventPublisher activatingPublisher EventPublisher deactivatingPublisher EventPublisher startingPublisher EventPublisher titleChangedPublisher EventPublisher iconChangedPublisher EventPublisher progressIndicator *ProgressIndicator icon Image prevFocusHWnd win.HWND proposedSize Size // in native pixels closeReason CloseReason inSizingLoop bool startingLayoutViaSizingLoop bool isInRestoreState bool started bool layoutScheduled bool } func (fb *FormBase) init(form Form) error { var err error if fb.clientComposite, err = NewComposite(form); err != nil { return err } fb.clientComposite.SetName("clientComposite") fb.clientComposite.background = nil fb.clientComposite.children.observer = form.AsFormBase() fb.MustRegisterProperty("Icon", NewProperty( func() interface{} { return fb.Icon() }, func(v interface{}) error { icon, err := IconFrom(v, fb.DPI()) if err != nil { return err } var img Image if icon != nil { img = icon } fb.SetIcon(img) return nil }, fb.iconChangedPublisher.Event())) fb.MustRegisterProperty("Title", NewProperty( func() interface{} { return fb.Title() }, func(v interface{}) error { return fb.SetTitle(assertStringOr(v, "")) }, fb.titleChangedPublisher.Event())) version := win.GetVersion() if (version&0xFF) > 6 || ((version&0xFF) == 6 && (version&0xFF00>>8) > 0) { win.ChangeWindowMessageFilterEx(fb.hWnd, taskbarButtonCreatedMsgId, win.MSGFLT_ALLOW, nil) } fb.performLayout, fb.layoutResults, fb.inSizeLoop, fb.updateStopwatch, fb.quitLayoutPerformer = startLayoutPerformer(fb) return nil } func (fb *FormBase) Dispose() { if fb.hWnd != 0 { fb.quitLayoutPerformer <- struct{}{} } fb.WindowBase.Dispose() } func (fb *FormBase) AsContainerBase() *ContainerBase { if fb.clientComposite == nil { return nil } return fb.clientComposite.AsContainerBase() } func (fb *FormBase) AsFormBase() *FormBase { return fb } func (fb *FormBase) Children() *WidgetList { if fb.clientComposite == nil { return nil } return fb.clientComposite.Children() } func (fb *FormBase) Layout() Layout { if fb.clientComposite == nil { return nil } return fb.clientComposite.Layout() } func (fb *FormBase) SetLayout(value Layout) error { if fb.clientComposite == nil { return newError("clientComposite not initialized") } return fb.clientComposite.SetLayout(value) } func (fb *FormBase) SetBoundsPixels(bounds Rectangle) error { if layout := fb.Layout(); layout != nil { layoutItem := CreateLayoutItemsForContainer(fb) minSize := fb.sizeFromClientSizePixels(layoutItem.MinSizeForSize(bounds.Size())) minSize = fb.sizeFromClientSizePixels(layoutItem.MinSizeForSize(minSize)) if bounds.Width < minSize.Width { bounds.Width = minSize.Width } if bounds.Height < minSize.Height { bounds.Height = minSize.Height } } if err := fb.WindowBase.SetBoundsPixels(bounds); err != nil { return err } fb.proposedSize = bounds.Size() return nil } func (fb *FormBase) fixedSize() bool { return !fb.hasStyleBits(win.WS_THICKFRAME) } func (fb *FormBase) DataBinder() *DataBinder { return fb.clientComposite.DataBinder() } func (fb *FormBase) SetDataBinder(db *DataBinder) { fb.clientComposite.SetDataBinder(db) } func (fb *FormBase) SetSuspended(suspended bool) { if suspended == fb.suspended { return } fb.suspended = suspended if fb.clientComposite != nil { fb.clientComposite.SetSuspended(suspended) } } func (fb *FormBase) MouseDown() *MouseEvent { return fb.clientComposite.MouseDown() } func (fb *FormBase) MouseMove() *MouseEvent { return fb.clientComposite.MouseMove() } func (fb *FormBase) MouseUp() *MouseEvent { return fb.clientComposite.MouseUp() } func (fb *FormBase) onInsertingWidget(index int, widget Widget) error { return fb.clientComposite.onInsertingWidget(index, widget) } func (fb *FormBase) onInsertedWidget(index int, widget Widget) error { return fb.clientComposite.onInsertedWidget(index, widget) } func (fb *FormBase) onRemovingWidget(index int, widget Widget) error { return fb.clientComposite.onRemovingWidget(index, widget) } func (fb *FormBase) onRemovedWidget(index int, widget Widget) error { return fb.clientComposite.onRemovedWidget(index, widget) } func (fb *FormBase) onClearingWidgets() error { return fb.clientComposite.onClearingWidgets() } func (fb *FormBase) onClearedWidgets() error { return fb.clientComposite.onClearedWidgets() } func (fb *FormBase) ContextMenu() *Menu { return fb.clientComposite.ContextMenu() } func (fb *FormBase) SetContextMenu(contextMenu *Menu) { fb.clientComposite.SetContextMenu(contextMenu) } func (fb *FormBase) ContextMenuLocation() Point { return fb.clientComposite.ContextMenuLocation() } func (fb *FormBase) applyEnabled(enabled bool) { fb.WindowBase.applyEnabled(enabled) fb.clientComposite.applyEnabled(enabled) } func (fb *FormBase) applyFont(font *Font) { fb.WindowBase.applyFont(font) fb.clientComposite.applyFont(font) } func (fb *FormBase) ApplySysColors() { fb.WindowBase.ApplySysColors() fb.clientComposite.ApplySysColors() } func (fb *FormBase) Background() Brush { return fb.clientComposite.Background() } func (fb *FormBase) SetBackground(background Brush) { fb.clientComposite.SetBackground(background) } func (fb *FormBase) Title() string { return fb.text() } func (fb *FormBase) SetTitle(value string) error { return fb.setText(value) } func (fb *FormBase) TitleChanged() *Event { return fb.titleChangedPublisher.Event() } // RightToLeftLayout returns whether coordinates on the x axis of the // FormBase increase from right to left. func (fb *FormBase) RightToLeftLayout() bool { return fb.hasExtendedStyleBits(win.WS_EX_LAYOUTRTL) } // SetRightToLeftLayout sets whether coordinates on the x axis of the // FormBase increase from right to left. func (fb *FormBase) SetRightToLeftLayout(rtl bool) error { return fb.ensureExtendedStyleBits(win.WS_EX_LAYOUTRTL, rtl) } func (fb *FormBase) Run() int { if fb.owner != nil { win.EnableWindow(fb.owner.Handle(), false) invalidateDescendentBorders := func() { walkDescendants(fb.owner, func(wnd Window) bool { if widget, ok := wnd.(Widget); ok { widget.AsWidgetBase().invalidateBorderInParent() } return true }) } invalidateDescendentBorders() defer invalidateDescendentBorders() } fb.started = true fb.startingPublisher.Publish() fb.SetBoundsPixels(fb.BoundsPixels()) if fb.proposedSize == (Size{}) { fb.proposedSize = maxSize(SizeFrom96DPI(fb.minSize96dpi, fb.DPI()), fb.SizePixels()) if !fb.Suspended() { fb.startLayout() } } fb.SetSuspended(false) return fb.mainLoop() } func (fb *FormBase) handleKeyDown(msg *win.MSG) bool { ret := false key, mods := Key(msg.WParam), ModifiersDown() // Tabbing if key == KeyTab && (mods&ModControl) != 0 { doTabbing := func(tw *TabWidget) { index := tw.CurrentIndex() if (mods & ModShift) != 0 { index-- if index < 0 { index = tw.Pages().Len() - 1 } } else { index++ if index >= tw.Pages().Len() { index = 0 } } tw.SetCurrentIndex(index) } hwnd := win.GetFocus() LOOP: for hwnd != 0 { window := windowFromHandle(hwnd) switch widget := window.(type) { case nil: case *TabWidget: doTabbing(widget) return true case Widget: default: break LOOP } hwnd = win.GetParent(hwnd) } walkDescendants(fb.window, func(w Window) bool { if tw, ok := w.(*TabWidget); ok { doTabbing(tw) ret = true return false } return true }) if ret { return true } } // Shortcut actions hwnd := msg.HWnd for hwnd != 0 { if window := windowFromHandle(hwnd); window != nil { wb := window.AsWindowBase() if wb.shortcutActions != nil { for _, action := range wb.shortcutActions.actions { if action.shortcut.Key == key && action.shortcut.Modifiers == mods && action.Visible() && action.Enabled() { action.raiseTriggered() return true } } } } hwnd = win.GetParent(hwnd) } // WebView walkDescendants(fb.window, func(w Window) bool { if webView, ok := w.(*WebView); ok { webViewHWnd := webView.Handle() if webViewHWnd == msg.HWnd || win.IsChild(webViewHWnd, msg.HWnd) { _ret := webView.translateAccelerator(msg) if _ret { ret = _ret } } } return true }) return ret } func (fb *FormBase) Starting() *Event { return fb.startingPublisher.Event() } func (fb *FormBase) Activating() *Event { return fb.activatingPublisher.Event() } func (fb *FormBase) Deactivating() *Event { return fb.deactivatingPublisher.Event() } func (fb *FormBase) Activate() error { if hwndPrevActive := win.SetActiveWindow(fb.hWnd); hwndPrevActive == 0 { return lastError("SetActiveWindow") } return nil } func (fb *FormBase) Owner() Form { return fb.owner } func (fb *FormBase) SetOwner(value Form) error { fb.owner = value var ownerHWnd win.HWND if value != nil { ownerHWnd = value.Handle() } win.SetLastError(0) if 0 == win.SetWindowLong( fb.hWnd, win.GWL_HWNDPARENT, int32(ownerHWnd)) && win.GetLastError() != 0 { return lastError("SetWindowLong") } return nil } func (fb *FormBase) Icon() Image { return fb.icon } func (fb *FormBase) SetIcon(icon Image) error { var hIconSmall, hIconBig uintptr if icon != nil { dpi := fb.DPI() size96dpi := icon.Size() smallHeight := int(win.GetSystemMetricsForDpi(win.SM_CYSMICON, uint32(dpi))) smallDPI := int(math.Round(float64(smallHeight) / float64(size96dpi.Height) * 96.0)) smallIcon, err := iconCache.Icon(icon, smallDPI) if err != nil { return err } hIconSmall = uintptr(smallIcon.handleForDPI(smallDPI)) bigHeight := int(win.GetSystemMetricsForDpi(win.SM_CYICON, uint32(dpi))) bigDPI := int(math.Round(float64(bigHeight) / float64(size96dpi.Height) * 96.0)) bigIcon, err := iconCache.Icon(icon, bigDPI) if err != nil { return err } hIconBig = uintptr(bigIcon.handleForDPI(bigDPI)) } fb.SendMessage(win.WM_SETICON, 0, hIconSmall) fb.SendMessage(win.WM_SETICON, 1, hIconBig) fb.icon = icon fb.iconChangedPublisher.Publish() return nil } func (fb *FormBase) IconChanged() *Event { return fb.iconChangedPublisher.Event() } func (fb *FormBase) Hide() { fb.window.SetVisible(false) } func (fb *FormBase) Show() { fb.proposedSize = maxSize(SizeFrom96DPI(fb.minSize96dpi, fb.DPI()), fb.SizePixels()) if p, ok := fb.window.(Persistable); ok && p.Persistent() && App().Settings() != nil { p.RestoreState() } fb.window.SetVisible(true) } func (fb *FormBase) close() error { if p, ok := fb.window.(Persistable); ok && p.Persistent() && App().Settings() != nil { p.SaveState() } fb.window.Dispose() return nil } func (fb *FormBase) Close() error { fb.SendMessage(win.WM_CLOSE, 0, 0) return nil } func (fb *FormBase) Persistent() bool { return fb.clientComposite.persistent } func (fb *FormBase) SetPersistent(value bool) { fb.clientComposite.persistent = value } func (fb *FormBase) SaveState() error { if err := fb.clientComposite.SaveState(); err != nil { return err } var wp win.WINDOWPLACEMENT wp.Length = uint32(unsafe.Sizeof(wp)) if !win.GetWindowPlacement(fb.hWnd, &wp) { return lastError("GetWindowPlacement") } state := fmt.Sprint( wp.Flags, wp.ShowCmd, wp.PtMinPosition.X, wp.PtMinPosition.Y, wp.PtMaxPosition.X, wp.PtMaxPosition.Y, wp.RcNormalPosition.Left, wp.RcNormalPosition.Top, wp.RcNormalPosition.Right, wp.RcNormalPosition.Bottom) if err := fb.WriteState(state); err != nil { return err } return nil } func (fb *FormBase) RestoreState() error { if fb.isInRestoreState { return nil } fb.isInRestoreState = true defer func() { fb.isInRestoreState = false }() state, err := fb.ReadState() if err != nil { return err } if state == "" { return nil } var wp win.WINDOWPLACEMENT if _, err := fmt.Sscan(state, &wp.Flags, &wp.ShowCmd, &wp.PtMinPosition.X, &wp.PtMinPosition.Y, &wp.PtMaxPosition.X, &wp.PtMaxPosition.Y, &wp.RcNormalPosition.Left, &wp.RcNormalPosition.Top, &wp.RcNormalPosition.Right, &wp.RcNormalPosition.Bottom); err != nil { return err } wp.Length = uint32(unsafe.Sizeof(wp)) if layout := fb.Layout(); layout != nil && fb.fixedSize() { layoutItem := CreateLayoutItemsForContainer(fb) minSize := fb.sizeFromClientSizePixels(layoutItem.MinSize()) wp.RcNormalPosition.Right = wp.RcNormalPosition.Left + int32(minSize.Width) - 1 wp.RcNormalPosition.Bottom = wp.RcNormalPosition.Top + int32(minSize.Height) - 1 } if !win.SetWindowPlacement(fb.hWnd, &wp) { return lastError("SetWindowPlacement") } return fb.clientComposite.RestoreState() } func (fb *FormBase) Closing() *CloseEvent { return fb.closingPublisher.Event() } func (fb *FormBase) ProgressIndicator() *ProgressIndicator { return fb.progressIndicator } func (fb *FormBase) setStopwatch(sw *stopwatch) { fb.stopwatch = sw fb.updateStopwatch <- sw } func (fb *FormBase) startLayout() bool { if fb.performLayout == nil || fb.inSizingLoop && !fb.startingLayoutViaSizingLoop { return false } cs := fb.clientSizeFromSizePixels(fb.proposedSize) min := CreateLayoutItemsForContainer(fb.clientComposite).MinSizeForSize(fb.proposedSize) if cs.Width < min.Width || cs.Height < min.Height { cs = maxSize(cs, min) size := fb.sizeFromClientSizePixels(cs) fb.SetSizePixels(size) fb.Invalidate() } fb.clientComposite.SetBoundsPixels(Rectangle{Width: cs.Width, Height: cs.Height}) cli := CreateLayoutItemsForContainer(fb) cli.Geometry().ClientSize = cs fb.performLayout <- cli return true } func (fb *FormBase) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_ACTIVATE: switch win.LOWORD(uint32(wParam)) { case win.WA_ACTIVE, win.WA_CLICKACTIVE: if fb.prevFocusHWnd != 0 { win.SetFocus(fb.prevFocusHWnd) } fb.group.SetActiveForm(fb.window.(Form)) fb.activatingPublisher.Publish() case win.WA_INACTIVE: fb.prevFocusHWnd = win.GetFocus() fb.group.SetActiveForm(nil) fb.deactivatingPublisher.Publish() } return 0 case win.WM_CLOSE: fb.closeReason = CloseReasonUnknown var canceled bool fb.closingPublisher.Publish(&canceled, fb.closeReason) if !canceled { if fb.owner != nil { win.EnableWindow(fb.owner.Handle(), true) if !win.SetWindowPos(fb.owner.Handle(), win.HWND_NOTOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE|win.SWP_SHOWWINDOW) { lastError("SetWindowPos") } } fb.close() } return 0 case win.WM_COMMAND: return fb.clientComposite.WndProc(hwnd, msg, wParam, lParam) case win.WM_GETMINMAXINFO: if fb.Suspended() || fb.proposedSize == (Size{}) { break } mmi := (*win.MINMAXINFO)(unsafe.Pointer(lParam)) var min Size if layout := fb.clientComposite.layout; layout != nil { size := fb.clientSizeFromSizePixels(fb.proposedSize) layoutItem := CreateLayoutItemsForContainer(fb) min = fb.sizeFromClientSizePixels(layoutItem.MinSizeForSize(size)) if fb.proposedSize.Width < min.Width { min = fb.sizeFromClientSizePixels(layoutItem.MinSizeForSize(min)) } } minSize := SizeFrom96DPI(fb.minSize96dpi, fb.DPI()) mmi.PtMinTrackSize = Point{ maxi(min.Width, minSize.Width), maxi(min.Height, minSize.Height), }.toPOINT() return 0 case win.WM_NOTIFY: return fb.clientComposite.WndProc(hwnd, msg, wParam, lParam) case win.WM_SETTEXT: fb.titleChangedPublisher.Publish() case win.WM_ENTERSIZEMOVE: fb.inSizingLoop = true fb.inSizeLoop <- true case win.WM_EXITSIZEMOVE: fb.inSizingLoop = false fb.inSizeLoop <- false case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam)) if wp.Flags&win.SWP_SHOWWINDOW != 0 { fb.startLayout() } if wp.Flags&win.SWP_NOSIZE != 0 || fb.Layout() == nil || fb.Suspended() { break } fb.proposedSize = Size{int(wp.Cx), int(wp.Cy)} const performingLayoutSubject = "*FormBase.WndProc - WM_WINDOWPOSCHANGED - full layout from sizing loop" if fb.inSizingLoop { fb.startingLayoutViaSizingLoop = true if fb.stopwatch != nil { fb.stopwatch.Start(performingLayoutSubject) } } if fb.startLayout() { if fb.inSizingLoop { fb.startingLayoutViaSizingLoop = false applyLayoutResults(<-fb.layoutResults, fb.stopwatch) if fb.stopwatch != nil { fb.stopwatch.Stop(performingLayoutSubject) } } } case win.WM_SYSCOLORCHANGE: fb.ApplySysColors() case win.WM_DPICHANGED: wasSuspended := fb.Suspended() fb.SetSuspended(true) defer fb.SetSuspended(wasSuspended) dpi := int(win.HIWORD(uint32(wParam))) seenInApplyFontToDescendantsDuringDPIChange = make(map[*WindowBase]bool) seenInApplyDPIToDescendantsDuringDPIChange = make(map[*WindowBase]bool) defer func() { seenInApplyFontToDescendantsDuringDPIChange = nil seenInApplyDPIToDescendantsDuringDPIChange = nil }() fb.clientComposite.ApplyDPI(dpi) fb.ApplyDPI(dpi) if fb.progressIndicator != nil { fb.progressIndicator.SetOverlayIcon(fb.progressIndicator.overlayIcon, fb.progressIndicator.overlayIconDescription) } applyDPIToDescendants(fb.window, dpi) fb.SetSuspended(wasSuspended) rc := (*win.RECT)(unsafe.Pointer(lParam)) bounds := rectangleFromRECT(*rc) fb.proposedSize = bounds.Size() fb.window.SetBoundsPixels(bounds) fb.SetIcon(fb.icon) time.AfterFunc(time.Second, func() { if fb.hWnd == 0 { return } fb.Synchronize(func() { for ni := range notifyIcons { // We do this on all NotifyIcons, not just ones attached to this form or descendents, because // the notify icon might be on a different screen, and since it can't get notifications itself // we hope that one of the forms did for it. We also have to delay it by a second, because the // tray usually gets resized sometime after us. This is a nasty hack! ni.applyDPI() } }) }) case win.WM_SYSCOMMAND: if wParam == win.SC_CLOSE { fb.closeReason = CloseReasonUser } case taskbarButtonCreatedMsgId: version := win.GetVersion() major := version & 0xFF minor := version & 0xFF00 >> 8 // Check that the OS is Win 7 or later (Win 7 is v6.1). if fb.progressIndicator == nil && (major > 6 || (major == 6 && minor > 0)) { fb.progressIndicator, _ = newTaskbarList3(fb.hWnd) } } return fb.WindowBase.WndProc(hwnd, msg, wParam, lParam) }