// 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" "math/big" "reflect" "strconv" "syscall" "time" "unsafe" "github.com/lxn/win" ) type ComboBox struct { WidgetBase bindingValueProvider BindingValueProvider model ListModel providedModel interface{} bindingMember string displayMember string format string precision int itemsResetHandlerHandle int itemChangedHandlerHandle int itemsInsertedHandlerHandle int itemsRemovedHandlerHandle int maxItemTextWidth int // in native pixels prevCurIndex int selChangeIndex int maxLength int currentIndexChangedPublisher EventPublisher textChangedPublisher EventPublisher editingFinishedPublisher EventPublisher editOrigWndProcPtr uintptr editing bool persistent bool } var comboBoxEditWndProcPtr uintptr func init() { AppendToWalkInit(func() { comboBoxEditWndProcPtr = syscall.NewCallback(comboBoxEditWndProc) }) } func comboBoxEditWndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { cb := (*ComboBox)(unsafe.Pointer(win.GetWindowLongPtr(hwnd, win.GWLP_USERDATA))) switch msg { case win.WM_GETDLGCODE: if !cb.editing { if form := ancestor(cb); form != nil { if dlg, ok := form.(dialogish); ok { if dlg.DefaultButton() != nil { // If the ComboBox lives in a Dialog that has a // DefaultButton, we won't swallow the return key. break } } } } if wParam == win.VK_RETURN { return win.DLGC_WANTALLKEYS } case win.WM_KEYDOWN: if wParam != win.VK_RETURN || 0 == cb.SendMessage(win.CB_GETDROPPEDSTATE, 0, 0) { cb.handleKeyDown(wParam, lParam) } if cb.editing && wParam == win.VK_RETURN { cb.editing = false cb.editingFinishedPublisher.Publish() } case win.WM_KEYUP: if wParam != win.VK_RETURN || 0 == cb.SendMessage(win.CB_GETDROPPEDSTATE, 0, 0) { cb.handleKeyUp(wParam, lParam) } case win.WM_SETFOCUS, win.WM_KILLFOCUS: cb.invalidateBorderInParent() if cb.editing && msg == win.WM_KILLFOCUS { cb.editing = false cb.editingFinishedPublisher.Publish() } } return win.CallWindowProc(cb.editOrigWndProcPtr, hwnd, msg, wParam, lParam) } func NewComboBox(parent Container) (*ComboBox, error) { cb, err := newComboBoxWithStyle(parent, win.CBS_AUTOHSCROLL|win.CBS_DROPDOWN) if err != nil { return nil, err } editHwnd := win.GetWindow(cb.hWnd, win.GW_CHILD) win.SetWindowLongPtr(editHwnd, win.GWLP_USERDATA, uintptr(unsafe.Pointer(cb))) cb.editOrigWndProcPtr = win.SetWindowLongPtr(editHwnd, win.GWLP_WNDPROC, comboBoxEditWndProcPtr) return cb, nil } func NewDropDownBox(parent Container) (*ComboBox, error) { return newComboBoxWithStyle(parent, win.CBS_DROPDOWNLIST) } func newComboBoxWithStyle(parent Container, style uint32) (*ComboBox, error) { cb := &ComboBox{prevCurIndex: -1, selChangeIndex: -1, precision: 2} if err := InitWidget( cb, parent, "COMBOBOX", win.WS_TABSTOP|win.WS_VISIBLE|win.WS_VSCROLL|style, 0); err != nil { return nil, err } succeeded := false defer func() { if !succeeded { cb.Dispose() } }() var event *Event if style&win.CBS_DROPDOWNLIST == win.CBS_DROPDOWNLIST { event = cb.CurrentIndexChanged() } else { event = cb.TextChanged() } cb.GraphicsEffects().Add(InteractionEffect) cb.GraphicsEffects().Add(FocusEffect) cb.MustRegisterProperty("CurrentIndex", NewProperty( func() interface{} { return cb.CurrentIndex() }, func(v interface{}) error { return cb.SetCurrentIndex(assertIntOr(v, -1)) }, cb.CurrentIndexChanged())) cb.MustRegisterProperty("Text", NewProperty( func() interface{} { return cb.Text() }, func(v interface{}) error { return cb.SetText(assertStringOr(v, "")) }, event)) cb.MustRegisterProperty("CurrentItem", NewReadOnlyProperty( func() interface{} { if rlm, ok := cb.providedModel.(ReflectListModel); ok { if i := cb.CurrentIndex(); i > -1 { return reflect.ValueOf(rlm.Items()).Index(i).Interface() } } return nil }, cb.CurrentIndexChanged())) cb.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty( func() bool { return cb.CurrentIndex() != -1 }, cb.CurrentIndexChanged())) cb.MustRegisterProperty("TextNotEmpty", NewReadOnlyBoolProperty( func() bool { return cb.Text() != "" }, cb.CurrentIndexChanged())) cb.MustRegisterProperty("Value", NewProperty( func() interface{} { if cb.Editable() { return cb.Text() } index := cb.CurrentIndex() if cb.bindingValueProvider == nil || index == -1 { return nil } return cb.bindingValueProvider.BindingValue(index) }, func(v interface{}) error { if cb.Editable() { return cb.SetText(assertStringOr(v, "")) } if cb.bindingValueProvider == nil { if cb.model == nil { return nil } else { return newError("Data binding is only supported using a model that implements BindingValueProvider.") } } index := -1 count := cb.model.ItemCount() for i := 0; i < count; i++ { if cb.bindingValueProvider.BindingValue(i) == v { index = i break } } return cb.SetCurrentIndex(index) }, event)) succeeded = true return cb, nil } func (cb *ComboBox) applyFont(font *Font) { cb.WidgetBase.applyFont(font) if cb.model != nil { cb.maxItemTextWidth = cb.calculateMaxItemTextWidth() cb.RequestLayout() } } func (cb *ComboBox) Editable() bool { return !cb.hasStyleBits(win.CBS_DROPDOWNLIST) } func (cb *ComboBox) itemString(index int) string { switch val := cb.model.Value(index).(type) { case string: return val case time.Time: return val.Format(cb.format) case *big.Rat: return val.FloatString(cb.precision) default: return fmt.Sprintf(cb.format, val) } panic("unreachable") } func (cb *ComboBox) insertItemAt(index int) error { str := cb.itemString(index) lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str))) if win.CB_ERR == cb.SendMessage(win.CB_INSERTSTRING, uintptr(index), lp) { return newError("SendMessage(CB_INSERTSTRING)") } return nil } func (cb *ComboBox) removeItem(index int) error { if win.CB_ERR == cb.SendMessage(win.CB_DELETESTRING, uintptr(index), 0) { return newError("SendMessage(CB_DELETESTRING") } return nil } func (cb *ComboBox) resetItems() error { cb.SetSuspended(true) defer cb.SetSuspended(false) cb.selChangeIndex = -1 if win.FALSE == cb.SendMessage(win.CB_RESETCONTENT, 0, 0) { return newError("SendMessage(CB_RESETCONTENT)") } cb.maxItemTextWidth = 0 cb.SetCurrentIndex(-1) if cb.model == nil { return nil } count := cb.model.ItemCount() for i := 0; i < count; i++ { if err := cb.insertItemAt(i); err != nil { return err } } cb.RequestLayout() return nil } func (cb *ComboBox) attachModel() { itemsResetHandler := func() { cb.resetItems() } cb.itemsResetHandlerHandle = cb.model.ItemsReset().Attach(itemsResetHandler) itemChangedHandler := func(index int) { if win.CB_ERR == cb.SendMessage(win.CB_DELETESTRING, uintptr(index), 0) { newError("SendMessage(CB_DELETESTRING)") } cb.insertItemAt(index) cb.SetCurrentIndex(cb.prevCurIndex) } cb.itemChangedHandlerHandle = cb.model.ItemChanged().Attach(itemChangedHandler) cb.itemsInsertedHandlerHandle = cb.model.ItemsInserted().Attach(func(from, to int) { for i := from; i <= to; i++ { cb.insertItemAt(i) } }) cb.itemsRemovedHandlerHandle = cb.model.ItemsRemoved().Attach(func(from, to int) { for i := to; i >= from; i-- { cb.removeItem(i) } }) } func (cb *ComboBox) detachModel() { cb.model.ItemsReset().Detach(cb.itemsResetHandlerHandle) cb.model.ItemChanged().Detach(cb.itemChangedHandlerHandle) cb.model.ItemsInserted().Detach(cb.itemsInsertedHandlerHandle) cb.model.ItemsRemoved().Detach(cb.itemsRemovedHandlerHandle) } // Model returns the model of the ComboBox. func (cb *ComboBox) Model() interface{} { return cb.providedModel } // SetModel sets the model of the ComboBox. // // It is required that mdl either implements walk.ListModel or // walk.ReflectListModel or be a slice of pointers to struct or a []string. func (cb *ComboBox) SetModel(mdl interface{}) error { model, ok := mdl.(ListModel) if !ok && mdl != nil { var err error if model, err = newReflectListModel(mdl); err != nil { return err } if _, ok := mdl.([]string); !ok { if badms, ok := model.(bindingAndDisplayMemberSetter); ok { var bindingMember string if cb.Editable() { bindingMember = cb.displayMember } else { bindingMember = cb.bindingMember } badms.setBindingMember(bindingMember) badms.setDisplayMember(cb.displayMember) } } } cb.providedModel = mdl if cb.model != nil { cb.detachModel() } cb.model = model cb.bindingValueProvider, _ = model.(BindingValueProvider) if model != nil { cb.attachModel() } if err := cb.resetItems(); err != nil { return err } if !cb.Editable() && model != nil && model.ItemCount() == 1 { cb.SetCurrentIndex(0) } return cb.Invalidate() } // BindingMember returns the member from the model of the ComboBox that is bound // to a field of the data source managed by an associated DataBinder. // // This is only applicable to walk.ReflectListModel models and simple slices of // pointers to struct. func (cb *ComboBox) BindingMember() string { return cb.bindingMember } // SetBindingMember sets the member from the model of the ComboBox that is bound // to a field of the data source managed by an associated DataBinder. // // This is only applicable to walk.ReflectListModel models and simple slices of // pointers to struct. // // For a model consisting of items of type S, data source field of type T and // bindingMember "Foo", this can be one of the following: // // A field Foo T // A method func (s S) Foo() T // A method func (s S) Foo() (T, error) // // If bindingMember is not a simple member name like "Foo", but a path to a // member like "A.B.Foo", members "A" and "B" both must be one of the options // mentioned above, but with T having type pointer to struct. func (cb *ComboBox) SetBindingMember(bindingMember string) error { if bindingMember != "" { if _, ok := cb.providedModel.([]string); ok { return newError("invalid for []string model") } } cb.bindingMember = bindingMember if badms, ok := cb.model.(bindingAndDisplayMemberSetter); ok { badms.setBindingMember(bindingMember) } return nil } // DisplayMember returns the member from the model of the ComboBox that is // displayed in the ComboBox. // // This is only applicable to walk.ReflectListModel models and simple slices of // pointers to struct. func (cb *ComboBox) DisplayMember() string { return cb.displayMember } // SetDisplayMember sets the member from the model of the ComboBox that is // displayed in the ComboBox. // // This is only applicable to walk.ReflectListModel models and simple slices of // pointers to struct. // // For a model consisting of items of type S, the type of the specified member T // and displayMember "Foo", this can be one of the following: // // A field Foo T // A method func (s S) Foo() T // A method func (s S) Foo() (T, error) // // If displayMember is not a simple member name like "Foo", but a path to a // member like "A.B.Foo", members "A" and "B" both must be one of the options // mentioned above, but with T having type pointer to struct. func (cb *ComboBox) SetDisplayMember(displayMember string) error { if displayMember != "" { if _, ok := cb.providedModel.([]string); ok { return newError("invalid for []string model") } } cb.displayMember = displayMember if badms, ok := cb.model.(bindingAndDisplayMemberSetter); ok { badms.setDisplayMember(displayMember) } return nil } func (cb *ComboBox) Format() string { return cb.format } func (cb *ComboBox) SetFormat(value string) { cb.format = value } func (cb *ComboBox) Precision() int { return cb.precision } func (cb *ComboBox) SetPrecision(value int) { cb.precision = value } func (cb *ComboBox) MaxLength() int { return cb.maxLength } func (cb *ComboBox) SetMaxLength(value int) { cb.SendMessage(win.CB_LIMITTEXT, uintptr(value), 0) cb.maxLength = value } // calculateMaxItemTextWidth returns maximum item text width in native pixels. func (cb *ComboBox) calculateMaxItemTextWidth() int { hdc := win.GetDC(cb.hWnd) if hdc == 0 { newError("GetDC failed") return -1 } defer win.ReleaseDC(cb.hWnd, hdc) hFontOld := win.SelectObject(hdc, win.HGDIOBJ(cb.Font().handleForDPI(cb.DPI()))) defer win.SelectObject(hdc, hFontOld) var maxWidth int count := cb.model.ItemCount() for i := 0; i < count; i++ { var s win.SIZE str := syscall.StringToUTF16(cb.itemString(i)) if !win.GetTextExtentPoint32(hdc, &str[0], int32(len(str)-1), &s) { newError("GetTextExtentPoint32 failed") return -1 } maxWidth = maxi(maxWidth, int(s.CX)) } return maxWidth } func (cb *ComboBox) CurrentIndex() int { return int(int32(cb.SendMessage(win.CB_GETCURSEL, 0, 0))) } func (cb *ComboBox) SetCurrentIndex(value int) error { index := int(int32(cb.SendMessage(win.CB_SETCURSEL, uintptr(value), 0))) if index != value { return newError("invalid index") } if value != cb.prevCurIndex { cb.prevCurIndex = value cb.currentIndexChangedPublisher.Publish() } return nil } func (cb *ComboBox) CurrentIndexChanged() *Event { return cb.currentIndexChangedPublisher.Event() } func (cb *ComboBox) Text() string { return cb.text() } func (cb *ComboBox) SetText(value string) error { if err := cb.setText(value); err != nil { return err } cb.textChangedPublisher.Publish() return nil } func (cb *ComboBox) TextSelection() (start, end int) { cb.SendMessage(win.CB_GETEDITSEL, uintptr(unsafe.Pointer(&start)), uintptr(unsafe.Pointer(&end))) return } func (cb *ComboBox) SetTextSelection(start, end int) { cb.SendMessage(win.CB_SETEDITSEL, 0, uintptr(win.MAKELONG(uint16(start), uint16(end)))) } func (cb *ComboBox) TextChanged() *Event { return cb.textChangedPublisher.Event() } func (cb *ComboBox) EditingFinished() *Event { return cb.editingFinishedPublisher.Event() } func (cb *ComboBox) Persistent() bool { return cb.persistent } func (cb *ComboBox) SetPersistent(value bool) { cb.persistent = value } func (cb *ComboBox) SaveState() error { cb.WriteState(strconv.Itoa(cb.CurrentIndex())) return nil } func (cb *ComboBox) RestoreState() error { state, err := cb.ReadState() if err != nil { return err } if state == "" { return nil } if i, err := strconv.Atoi(state); err == nil { cb.SetCurrentIndex(i) } return nil } func (cb *ComboBox) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_COMMAND: code := win.HIWORD(uint32(wParam)) selIndex := cb.CurrentIndex() switch code { case win.CBN_EDITCHANGE: cb.editing = true cb.selChangeIndex = -1 cb.textChangedPublisher.Publish() case win.CBN_SELCHANGE: cb.selChangeIndex = selIndex case win.CBN_SELENDCANCEL: if cb.selChangeIndex != -1 { if cb.selChangeIndex < cb.model.ItemCount() { cb.SetCurrentIndex(cb.selChangeIndex) } cb.selChangeIndex = -1 } case win.CBN_SELENDOK: if editable := cb.Editable(); editable || selIndex != cb.prevCurIndex { if editable && selIndex > -1 { cb.Property("Value").Set(cb.model.Value(selIndex)) } cb.currentIndexChangedPublisher.Publish() cb.prevCurIndex = selIndex return 0 } cb.selChangeIndex = -1 } case win.WM_MOUSEWHEEL: if !cb.Enabled() { return 0 } case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam)) if wp.Flags&win.SWP_NOSIZE != 0 { break } if cb.Editable() { result := cb.WidgetBase.WndProc(hwnd, msg, wParam, lParam) cb.SetTextSelection(0, 0) return result } } return cb.WidgetBase.WndProc(hwnd, msg, wParam, lParam) } func (*ComboBox) NeedsWmSize() bool { return true } func (cb *ComboBox) CreateLayoutItem(ctx *LayoutContext) LayoutItem { var layoutFlags LayoutFlags if cb.Editable() { layoutFlags = GrowableHorz | GreedyHorz } else { layoutFlags = GrowableHorz } defaultSize := cb.dialogBaseUnitsToPixels(Size{30, 12}) if cb.model != nil && cb.maxItemTextWidth <= 0 { cb.maxItemTextWidth = cb.calculateMaxItemTextWidth() } // FIXME: Use GetThemePartSize instead of guessing w := maxi(defaultSize.Width, cb.maxItemTextWidth+int(win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, uint32(ctx.dpi)))+8) h := defaultSize.Height + 1 return &comboBoxLayoutItem{ layoutFlags: layoutFlags, idealSize: Size{w, h}, } } type comboBoxLayoutItem struct { LayoutItemBase layoutFlags LayoutFlags idealSize Size // in native pixels } func (li *comboBoxLayoutItem) LayoutFlags() LayoutFlags { return li.layoutFlags } func (li *comboBoxLayoutItem) IdealSize() Size { return li.idealSize } func (li *comboBoxLayoutItem) MinSize() Size { return li.idealSize }