// Copyright 2010 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" "syscall" "unsafe" "github.com/lxn/win" ) type ToolBarButtonStyle int const ( ToolBarButtonImageOnly ToolBarButtonStyle = iota ToolBarButtonTextOnly ToolBarButtonImageBeforeText ToolBarButtonImageAboveText ) type ToolBar struct { WidgetBase imageList *ImageList actions *ActionList defaultButtonWidth int maxTextRows int buttonStyle ToolBarButtonStyle } func NewToolBarWithOrientationAndButtonStyle(parent Container, orientation Orientation, buttonStyle ToolBarButtonStyle) (*ToolBar, error) { var style uint32 if orientation == Vertical { style = win.CCS_VERT | win.CCS_NORESIZE } else { style = win.TBSTYLE_WRAPABLE } if buttonStyle != ToolBarButtonImageAboveText { style |= win.TBSTYLE_LIST } tb := &ToolBar{ buttonStyle: buttonStyle, } tb.actions = newActionList(tb) if orientation == Vertical { tb.defaultButtonWidth = 100 } if err := InitWidget( tb, parent, "ToolbarWindow32", win.CCS_NODIVIDER|win.TBSTYLE_FLAT|win.TBSTYLE_TOOLTIPS|style, 0); err != nil { return nil, err } exStyle := tb.SendMessage(win.TB_GETEXTENDEDSTYLE, 0, 0) exStyle |= win.TBSTYLE_EX_DRAWDDARROWS | win.TBSTYLE_EX_MIXEDBUTTONS tb.SendMessage(win.TB_SETEXTENDEDSTYLE, 0, exStyle) return tb, nil } func NewToolBar(parent Container) (*ToolBar, error) { return NewToolBarWithOrientationAndButtonStyle(parent, Horizontal, ToolBarButtonImageOnly) } func NewVerticalToolBar(parent Container) (*ToolBar, error) { return NewToolBarWithOrientationAndButtonStyle(parent, Vertical, ToolBarButtonImageAboveText) } func (tb *ToolBar) Dispose() { tb.WidgetBase.Dispose() tb.actions.Clear() if tb.imageList != nil { tb.imageList.Dispose() tb.imageList = nil } } func (tb *ToolBar) applyFont(font *Font) { tb.WidgetBase.applyFont(font) tb.applyDefaultButtonWidth() tb.RequestLayout() } func (tb *ToolBar) ApplyDPI(dpi int) { tb.WidgetBase.ApplyDPI(dpi) var maskColor Color var size Size if tb.imageList != nil { maskColor = tb.imageList.maskColor size = SizeFrom96DPI(tb.imageList.imageSize96dpi, dpi) } else { size = SizeFrom96DPI(Size{16, 16}, dpi) } iml, err := NewImageListForDPI(size, maskColor, dpi) if err != nil { return } tb.SendMessage(win.TB_SETIMAGELIST, 0, uintptr(iml.hIml)) if tb.imageList != nil { tb.imageList.Dispose() } tb.imageList = iml for _, action := range tb.actions.actions { if action.image != nil { tb.onActionChanged(action) } } tb.hFont = tb.Font().handleForDPI(tb.DPI()) setWindowFont(tb.hWnd, tb.hFont) } func (tb *ToolBar) Orientation() Orientation { style := win.GetWindowLong(tb.hWnd, win.GWL_STYLE) if style&win.CCS_VERT > 0 { return Vertical } return Horizontal } func (tb *ToolBar) ButtonStyle() ToolBarButtonStyle { return tb.buttonStyle } func (tb *ToolBar) applyDefaultButtonWidth() error { if tb.defaultButtonWidth == 0 { return nil } dpi := tb.DPI() width := IntFrom96DPI(tb.defaultButtonWidth, dpi) lParam := uintptr(win.MAKELONG(uint16(width), uint16(width))) if 0 == tb.SendMessage(win.TB_SETBUTTONWIDTH, 0, lParam) { return newError("SendMessage(TB_SETBUTTONWIDTH)") } size := uint32(tb.SendMessage(win.TB_GETBUTTONSIZE, 0, 0)) height := win.HIWORD(size) lParam = uintptr(win.MAKELONG(uint16(width), height)) if win.FALSE == tb.SendMessage(win.TB_SETBUTTONSIZE, 0, lParam) { return newError("SendMessage(TB_SETBUTTONSIZE)") } return nil } // DefaultButtonWidth returns the default button width of the ToolBar. // // The default value for a horizontal ToolBar is 0, resulting in automatic // sizing behavior. For a vertical ToolBar, the default is 100 pixels. func (tb *ToolBar) DefaultButtonWidth() int { return tb.defaultButtonWidth } // SetDefaultButtonWidth sets the default button width of the ToolBar. // // Calling this method affects all buttons in the ToolBar, no matter if they are // added before or after the call. A width of 0 results in automatic sizing // behavior. Negative values are not allowed. func (tb *ToolBar) SetDefaultButtonWidth(width int) error { if width == tb.defaultButtonWidth { return nil } if width < 0 { return newError("width must be >= 0") } old := tb.defaultButtonWidth tb.defaultButtonWidth = width for _, action := range tb.actions.actions { if err := tb.onActionChanged(action); err != nil { tb.defaultButtonWidth = old return err } } return tb.applyDefaultButtonWidth() } func (tb *ToolBar) MaxTextRows() int { return tb.maxTextRows } func (tb *ToolBar) SetMaxTextRows(maxTextRows int) error { if 0 == tb.SendMessage(win.TB_SETMAXTEXTROWS, uintptr(maxTextRows), 0) { return newError("SendMessage(TB_SETMAXTEXTROWS)") } tb.maxTextRows = maxTextRows return nil } func (tb *ToolBar) Actions() *ActionList { return tb.actions } func (tb *ToolBar) ImageList() *ImageList { return tb.imageList } func (tb *ToolBar) SetImageList(value *ImageList) { var hIml win.HIMAGELIST if tb.buttonStyle != ToolBarButtonTextOnly && value != nil { hIml = value.hIml } tb.SendMessage(win.TB_SETIMAGELIST, 0, uintptr(hIml)) tb.imageList = value } func (tb *ToolBar) imageIndex(image Image) (imageIndex int32, err error) { if tb.imageList == nil { dpi := tb.DPI() iml, err := NewImageListForDPI(SizeFrom96DPI(Size{16, 16}, dpi), 0, dpi) if err != nil { return 0, err } tb.SetImageList(iml) } imageIndex = -1 if image != nil { if imageIndex, err = tb.imageList.AddImage(image); err != nil { return } } return } func (tb *ToolBar) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_MOUSEMOVE, win.WM_MOUSELEAVE, win.WM_LBUTTONDOWN: tb.Invalidate() case win.WM_COMMAND: switch win.HIWORD(uint32(wParam)) { case win.BN_CLICKED: actionId := uint16(win.LOWORD(uint32(wParam))) if action, ok := actionsById[actionId]; ok { action.raiseTriggered() return 0 } } case win.WM_NOTIFY: nmhdr := (*win.NMHDR)(unsafe.Pointer(lParam)) switch int32(nmhdr.Code) { case win.TBN_DROPDOWN: nmtb := (*win.NMTOOLBAR)(unsafe.Pointer(lParam)) actionId := uint16(nmtb.IItem) if action := actionsById[actionId]; action != nil { var r win.RECT if 0 == tb.SendMessage(win.TB_GETRECT, uintptr(actionId), uintptr(unsafe.Pointer(&r))) { break } p := win.POINT{r.Left, r.Bottom} if !win.ClientToScreen(tb.hWnd, &p) { break } action.menu.updateItemsWithImageForWindow(tb) win.TrackPopupMenuEx( action.menu.hMenu, win.TPM_NOANIMATION, p.X, p.Y, tb.hWnd, nil) return win.TBDDRET_DEFAULT } } case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam)) if wp.Flags&win.SWP_NOSIZE != 0 { break } tb.SendMessage(win.TB_AUTOSIZE, 0, 0) } return tb.WidgetBase.WndProc(hwnd, msg, wParam, lParam) } func (tb *ToolBar) initButtonForAction(action *Action, state, style *byte, image *int32, text *uintptr) (err error) { if tb.hasStyleBits(win.CCS_VERT) { *state |= win.TBSTATE_WRAP } else if tb.defaultButtonWidth == 0 { *style |= win.BTNS_AUTOSIZE } if action.checked { *state |= win.TBSTATE_CHECKED } if action.enabled { *state |= win.TBSTATE_ENABLED } if action.checkable { *style |= win.BTNS_CHECK } if action.exclusive { *style |= win.BTNS_GROUP } if tb.buttonStyle != ToolBarButtonImageOnly && len(action.text) > 0 { *style |= win.BTNS_SHOWTEXT } if action.menu != nil { if len(action.Triggered().handlers) > 0 { *style |= win.BTNS_DROPDOWN } else { *style |= win.BTNS_WHOLEDROPDOWN } } if action.IsSeparator() { *style = win.BTNS_SEP } if tb.buttonStyle != ToolBarButtonTextOnly { if *image, err = tb.imageIndex(action.image); err != nil { return err } } var actionText string if s := action.shortcut; tb.buttonStyle == ToolBarButtonImageOnly && s.Key != 0 { actionText = fmt.Sprintf("%s (%s)", action.Text(), s.String()) } else { actionText = action.Text() } if len(actionText) != 0 { *text = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(actionText))) } else if len(action.toolTip) != 0 { *text = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(action.toolTip))) } return } func (tb *ToolBar) onActionChanged(action *Action) error { tbbi := win.TBBUTTONINFO{ DwMask: win.TBIF_IMAGE | win.TBIF_STATE | win.TBIF_STYLE | win.TBIF_TEXT, IImage: win.I_IMAGENONE, } tbbi.CbSize = uint32(unsafe.Sizeof(tbbi)) if err := tb.initButtonForAction( action, &tbbi.FsState, &tbbi.FsStyle, &tbbi.IImage, &tbbi.PszText); err != nil { return err } if 0 == tb.SendMessage( win.TB_SETBUTTONINFO, uintptr(action.id), uintptr(unsafe.Pointer(&tbbi))) { return newError("SendMessage(TB_SETBUTTONINFO) failed") } tb.RequestLayout() return nil } func (tb *ToolBar) onActionVisibleChanged(action *Action) error { if !action.IsSeparator() { defer tb.actions.updateSeparatorVisibility() } if action.Visible() { return tb.insertAction(action, true) } return tb.removeAction(action, true) } func (tb *ToolBar) insertAction(action *Action, visibleChanged bool) (err error) { if !visibleChanged { action.addChangedHandler(tb) defer func() { if err != nil { action.removeChangedHandler(tb) } }() } if !action.Visible() { return } index := tb.actions.indexInObserver(action) tbb := win.TBBUTTON{ IdCommand: int32(action.id), } if err = tb.initButtonForAction( action, &tbb.FsState, &tbb.FsStyle, &tbb.IBitmap, &tbb.IString); err != nil { return } tb.SetVisible(true) tb.SendMessage(win.TB_BUTTONSTRUCTSIZE, uintptr(unsafe.Sizeof(tbb)), 0) if win.FALSE == tb.SendMessage(win.TB_INSERTBUTTON, uintptr(index), uintptr(unsafe.Pointer(&tbb))) { return newError("SendMessage(TB_ADDBUTTONS)") } if err = tb.applyDefaultButtonWidth(); err != nil { return } tb.SendMessage(win.TB_AUTOSIZE, 0, 0) tb.RequestLayout() return } func (tb *ToolBar) removeAction(action *Action, visibleChanged bool) error { index := tb.actions.indexInObserver(action) if !visibleChanged { action.removeChangedHandler(tb) } if 0 == tb.SendMessage(win.TB_DELETEBUTTON, uintptr(index), 0) { return newError("SendMessage(TB_DELETEBUTTON) failed") } tb.RequestLayout() return nil } func (tb *ToolBar) onInsertedAction(action *Action) error { return tb.insertAction(action, false) } func (tb *ToolBar) onRemovingAction(action *Action) error { return tb.removeAction(action, false) } func (tb *ToolBar) onClearingActions() error { for i := tb.actions.Len() - 1; i >= 0; i-- { if action := tb.actions.At(i); action.Visible() { if err := tb.onRemovingAction(action); err != nil { return err } } } return nil } func (tb *ToolBar) CreateLayoutItem(ctx *LayoutContext) LayoutItem { buttonSize := uint32(tb.SendMessage(win.TB_GETBUTTONSIZE, 0, 0)) dpi := tb.DPI() width := IntFrom96DPI(tb.defaultButtonWidth, dpi) if width == 0 { width = int(win.LOWORD(buttonSize)) } height := int(win.HIWORD(buttonSize)) var size win.SIZE var wp uintptr var layoutFlags LayoutFlags if tb.Orientation() == Vertical { wp = win.TRUE layoutFlags = ShrinkableVert | GrowableVert | GreedyVert } else { wp = win.FALSE // FIXME: Since reimplementation of BoxLayout we must use 0 here, // otherwise the ToolBar contained in MainWindow will eat half the space. //layoutFlags = ShrinkableHorz | GrowableHorz } if win.FALSE != tb.SendMessage(win.TB_GETIDEALSIZE, wp, uintptr(unsafe.Pointer(&size))) { if wp == win.TRUE { height = int(size.CY) } else { width = int(size.CX) } } return &toolBarLayoutItem{ layoutFlags: layoutFlags, idealSize: Size{width, height}, } } type toolBarLayoutItem struct { LayoutItemBase layoutFlags LayoutFlags idealSize Size // in native pixels } func (li *toolBarLayoutItem) LayoutFlags() LayoutFlags { return li.layoutFlags } func (li *toolBarLayoutItem) IdealSize() Size { return li.idealSize } func (li *toolBarLayoutItem) MinSize() Size { return li.idealSize }