erm/vendor/github.com/lxn/walk/combobox.go

737 lines
17 KiB
Go

// 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
}