erm/vendor/github.com/lxn/walk/listbox.go
2021-07-30 23:29:20 +01:00

753 lines
18 KiB
Go

// Copyright 2012 The Walk Authors. All rights reserved.
// Use of lb 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"
"syscall"
"time"
"unsafe"
"github.com/lxn/win"
)
type ListBox struct {
WidgetBase
model ListModel
providedModel interface{}
styler ListItemStyler
style ListItemStyle
dataMember string
format string
precision int
prevCurIndex int
itemsResetHandlerHandle int
itemChangedHandlerHandle int
itemsInsertedHandlerHandle int
itemsRemovedHandlerHandle int
maxItemTextWidth int // in native pixels
lastWidth int // in native pixels
lastWidthsMeasuredFor []int // in native pixels
currentIndexChangedPublisher EventPublisher
selectedIndexesChangedPublisher EventPublisher
itemActivatedPublisher EventPublisher
themeNormalBGColor Color
themeNormalTextColor Color
themeSelectedBGColor Color
themeSelectedTextColor Color
themeSelectedNotFocusedBGColor Color
trackingMouseEvent bool
}
func NewListBox(parent Container) (*ListBox, error) {
return NewListBoxWithStyle(parent, 0)
}
func NewListBoxWithStyle(parent Container, style uint32) (*ListBox, error) {
lb := new(ListBox)
err := InitWidget(
lb,
parent,
"LISTBOX",
win.WS_BORDER|win.WS_TABSTOP|win.WS_VISIBLE|win.WS_VSCROLL|win.WS_HSCROLL|win.LBS_NOINTEGRALHEIGHT|win.LBS_NOTIFY|style,
0)
if err != nil {
return nil, err
}
succeeded := false
defer func() {
if !succeeded {
lb.Dispose()
}
}()
lb.setTheme("Explorer")
lb.style.dpi = lb.DPI()
lb.ApplySysColors()
lb.GraphicsEffects().Add(InteractionEffect)
lb.GraphicsEffects().Add(FocusEffect)
lb.MustRegisterProperty("CurrentIndex", NewProperty(
func() interface{} {
return lb.CurrentIndex()
},
func(v interface{}) error {
return lb.SetCurrentIndex(assertIntOr(v, -1))
},
lb.CurrentIndexChanged()))
lb.MustRegisterProperty("CurrentItem", NewReadOnlyProperty(
func() interface{} {
if i := lb.CurrentIndex(); i > -1 {
if rm, ok := lb.providedModel.(reflectModel); ok {
return reflect.ValueOf(rm.Items()).Index(i).Interface()
}
}
return nil
},
lb.CurrentIndexChanged()))
lb.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty(
func() bool {
return lb.CurrentIndex() != -1
},
lb.CurrentIndexChanged()))
succeeded = true
return lb, nil
}
func (*ListBox) LayoutFlags() LayoutFlags {
return ShrinkableHorz | ShrinkableVert | GrowableHorz | GrowableVert | GreedyHorz | GreedyVert
}
func (lb *ListBox) ItemStyler() ListItemStyler {
return lb.styler
}
func (lb *ListBox) SetItemStyler(styler ListItemStyler) {
lb.styler = styler
}
func (lb *ListBox) ApplySysColors() {
lb.WidgetBase.ApplySysColors()
var hc win.HIGHCONTRAST
hc.CbSize = uint32(unsafe.Sizeof(hc))
if win.SystemParametersInfo(win.SPI_GETHIGHCONTRAST, hc.CbSize, unsafe.Pointer(&hc), 0) {
lb.style.highContrastActive = hc.DwFlags&win.HCF_HIGHCONTRASTON != 0
}
lb.themeNormalBGColor = Color(win.GetSysColor(win.COLOR_WINDOW))
lb.themeNormalTextColor = Color(win.GetSysColor(win.COLOR_WINDOWTEXT))
lb.themeSelectedBGColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHT))
lb.themeSelectedTextColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHTTEXT))
lb.themeSelectedNotFocusedBGColor = Color(win.GetSysColor(win.COLOR_BTNFACE))
}
func (lb *ListBox) ApplyDPI(dpi int) {
lb.style.dpi = dpi
lb.WidgetBase.ApplyDPI(dpi)
}
func (lb *ListBox) applyFont(font *Font) {
lb.WidgetBase.applyFont(font)
for i := range lb.lastWidthsMeasuredFor {
lb.lastWidthsMeasuredFor[i] = 0
}
}
func (lb *ListBox) itemString(index int) string {
switch val := lb.model.Value(index).(type) {
case string:
return val
case time.Time:
return val.Format(lb.format)
case *big.Rat:
return val.FloatString(lb.precision)
default:
return fmt.Sprintf(lb.format, val)
}
panic("unreachable")
}
//insert one item from list model
func (lb *ListBox) insertItemAt(index int) error {
str := lb.itemString(index)
lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str)))
ret := int(lb.SendMessage(win.LB_INSERTSTRING, uintptr(index), lp))
if ret == win.LB_ERRSPACE || ret == win.LB_ERR {
return newError("SendMessage(LB_INSERTSTRING)")
}
return nil
}
func (lb *ListBox) removeItem(index int) error {
if win.LB_ERR == int(lb.SendMessage(win.LB_DELETESTRING, uintptr(index), 0)) {
return newError("SendMessage(LB_DELETESTRING)")
}
return nil
}
// reread all the items from list model
func (lb *ListBox) resetItems() error {
lb.SetSuspended(true)
defer lb.SetSuspended(false)
lb.SendMessage(win.LB_RESETCONTENT, 0, 0)
lb.maxItemTextWidth = 0
lb.SetCurrentIndex(-1)
if lb.model == nil {
return nil
}
count := lb.model.ItemCount()
lb.lastWidthsMeasuredFor = make([]int, count)
for i := 0; i < count; i++ {
if err := lb.insertItemAt(i); err != nil {
return err
}
}
if lb.styler == nil {
// Update the listbox width (this sets the correct horizontal scrollbar).
sh := lb.idealSize()
lb.SendMessage(win.LB_SETHORIZONTALEXTENT, uintptr(sh.Width), 0)
}
return nil
}
func (lb *ListBox) ensureVisibleItemsHeightUpToDate() error {
if lb.styler == nil {
return nil
}
if !lb.Suspended() {
lb.SetSuspended(true)
defer lb.SetSuspended(false)
}
topIndex := int(lb.SendMessage(win.LB_GETTOPINDEX, 0, 0))
offset := maxi(0, topIndex-10)
count := lb.model.ItemCount()
var rc win.RECT
lb.SendMessage(win.LB_GETITEMRECT, uintptr(offset), uintptr(unsafe.Pointer(&rc)))
width := int(rc.Right - rc.Left)
offsetTop := int(rc.Top)
lbHeight := lb.HeightPixels()
var pastBottomCount int
for i := offset; i >= 0 && i < count; i++ {
if lb.lastWidthsMeasuredFor[i] == lb.lastWidth {
continue
}
lb.SendMessage(win.LB_GETITEMRECT, uintptr(i), uintptr(unsafe.Pointer(&rc)))
if int(rc.Top)-offsetTop > lbHeight {
if pastBottomCount++; pastBottomCount > 10 {
break
}
}
height := lb.styler.ItemHeight(i, width)
lb.SendMessage(win.LB_SETITEMHEIGHT, uintptr(i), uintptr(height))
lb.lastWidthsMeasuredFor[i] = lb.lastWidth
}
lb.SendMessage(win.LB_SETTOPINDEX, uintptr(topIndex), 0)
return nil
}
func (lb *ListBox) attachModel() {
itemsResetHandler := func() {
lb.resetItems()
}
lb.itemsResetHandlerHandle = lb.model.ItemsReset().Attach(itemsResetHandler)
itemChangedHandler := func(index int) {
if win.CB_ERR == lb.SendMessage(win.LB_DELETESTRING, uintptr(index), 0) {
newError("SendMessage(CB_DELETESTRING)")
}
lb.insertItemAt(index)
if lb.styler != nil {
var rc win.RECT
lb.SendMessage(win.LB_GETITEMRECT, uintptr(index), uintptr(unsafe.Pointer(&rc)))
width := int(rc.Right - rc.Left)
height := lb.styler.ItemHeight(index, width)
lb.SendMessage(win.LB_SETITEMHEIGHT, uintptr(index), uintptr(height))
lb.lastWidthsMeasuredFor[index] = lb.lastWidth
}
lb.SetCurrentIndex(lb.prevCurIndex)
}
lb.itemChangedHandlerHandle = lb.model.ItemChanged().Attach(itemChangedHandler)
lb.itemsInsertedHandlerHandle = lb.model.ItemsInserted().Attach(func(from, to int) {
if !lb.Suspended() {
lb.SetSuspended(true)
defer lb.SetSuspended(false)
}
for i := from; i <= to; i++ {
lb.insertItemAt(i)
}
lb.lastWidthsMeasuredFor = append(lb.lastWidthsMeasuredFor[:from], append(make([]int, to-from+1), lb.lastWidthsMeasuredFor[from:]...)...)
lb.ensureVisibleItemsHeightUpToDate()
})
lb.itemsRemovedHandlerHandle = lb.model.ItemsRemoved().Attach(func(from, to int) {
if !lb.Suspended() {
lb.SetSuspended(true)
defer lb.SetSuspended(false)
}
for i := to; i >= from; i-- {
lb.removeItem(i)
}
lb.lastWidthsMeasuredFor = append(lb.lastWidthsMeasuredFor[:from], lb.lastWidthsMeasuredFor[to:]...)
lb.ensureVisibleItemsHeightUpToDate()
})
}
func (lb *ListBox) detachModel() {
lb.model.ItemsReset().Detach(lb.itemsResetHandlerHandle)
lb.model.ItemChanged().Detach(lb.itemChangedHandlerHandle)
lb.model.ItemsInserted().Detach(lb.itemsInsertedHandlerHandle)
lb.model.ItemsRemoved().Detach(lb.itemsRemovedHandlerHandle)
}
// Model returns the model of the ListBox.
func (lb *ListBox) Model() interface{} {
return lb.providedModel
}
// SetModel sets the model of the ListBox.
//
// It is required that mdl either implements walk.ListModel or
// walk.ReflectListModel or be a slice of pointers to struct or a []string.
func (lb *ListBox) 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 {
badms.setDisplayMember(lb.dataMember)
}
}
}
lb.providedModel = mdl
if lb.model != nil {
lb.detachModel()
}
lb.model = model
if model != nil {
lb.attachModel()
}
if err := lb.resetItems(); err != nil {
return err
}
return lb.ensureVisibleItemsHeightUpToDate()
}
// DataMember returns the member from the model of the ListBox that is displayed
// in the ListBox.
//
// This is only applicable to walk.ReflectListModel models and simple slices of
// pointers to struct.
func (lb *ListBox) DataMember() string {
return lb.dataMember
}
// SetDataMember sets the member from the model of the ListBox that is displayed
// in the ListBox.
//
// 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 dataMember "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 dataMember 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 (lb *ListBox) SetDataMember(dataMember string) error {
if dataMember != "" {
if _, ok := lb.providedModel.([]string); ok {
return newError("invalid for []string model")
}
}
lb.dataMember = dataMember
if badms, ok := lb.model.(bindingAndDisplayMemberSetter); ok {
badms.setDisplayMember(dataMember)
}
return nil
}
func (lb *ListBox) Format() string {
return lb.format
}
func (lb *ListBox) SetFormat(value string) {
lb.format = value
}
func (lb *ListBox) Precision() int {
return lb.precision
}
func (lb *ListBox) SetPrecision(value int) {
lb.precision = value
}
// calculateMaxItemTextWidth returns maximum item text width in native pixels.
func (lb *ListBox) calculateMaxItemTextWidth() int {
hdc := win.GetDC(lb.hWnd)
if hdc == 0 {
newError("GetDC failed")
return -1
}
defer win.ReleaseDC(lb.hWnd, hdc)
hFontOld := win.SelectObject(hdc, win.HGDIOBJ(lb.Font().handleForDPI(lb.DPI())))
defer win.SelectObject(hdc, hFontOld)
var maxWidth int
if lb.model == nil {
return -1
}
count := lb.model.ItemCount()
for i := 0; i < count; i++ {
item := lb.itemString(i)
var s win.SIZE
str := syscall.StringToUTF16(item)
if !win.GetTextExtentPoint32(hdc, &str[0], int32(len(str)-1), &s) {
newError("GetTextExtentPoint32 failed")
return -1
}
maxWidth = maxi(maxWidth, int(s.CX))
}
return maxWidth
}
// idealSize returns listbox ideal size in native pixels.
func (lb *ListBox) idealSize() Size {
defaultSize := lb.dialogBaseUnitsToPixels(Size{50, 12})
if lb.maxItemTextWidth <= 0 {
lb.maxItemTextWidth = lb.calculateMaxItemTextWidth()
}
// FIXME: Use GetThemePartSize instead of guessing
w := maxi(defaultSize.Width, lb.maxItemTextWidth+IntFrom96DPI(24, lb.DPI()))
h := defaultSize.Height + 1
return Size{w, h}
}
func (lb *ListBox) ItemVisible(index int) bool {
topIndex := int(lb.SendMessage(win.LB_GETTOPINDEX, 0, 0))
var rc win.RECT
lb.SendMessage(win.LB_GETITEMRECT, uintptr(index), uintptr(unsafe.Pointer(&rc)))
return index >= topIndex && int(rc.Top) < lb.HeightPixels()
}
func (lb *ListBox) EnsureItemVisible(index int) {
lb.SendMessage(win.LB_SETTOPINDEX, uintptr(index), 0)
}
func (lb *ListBox) CurrentIndex() int {
return int(int32(lb.SendMessage(win.LB_GETCURSEL, 0, 0)))
}
func (lb *ListBox) SetCurrentIndex(value int) error {
if value > -1 && win.LB_ERR == int(int32(lb.SendMessage(win.LB_SETCURSEL, uintptr(value), 0))) {
return newError("Invalid index or ensure lb is single-selection listbox")
}
if value != lb.prevCurIndex {
lb.prevCurIndex = value
lb.currentIndexChangedPublisher.Publish()
}
return nil
}
func (lb *ListBox) SelectedIndexes() []int {
count := int(int32(lb.SendMessage(win.LB_GETCOUNT, 0, 0)))
if count < 1 {
return nil
}
index32 := make([]int32, count)
if n := int(int32(lb.SendMessage(win.LB_GETSELITEMS, uintptr(count), uintptr(unsafe.Pointer(&index32[0]))))); n == win.LB_ERR {
return nil
} else {
indexes := make([]int, n)
for i := 0; i < n; i++ {
indexes[i] = int(index32[i])
}
return indexes
}
}
func (lb *ListBox) SetSelectedIndexes(indexes []int) {
var m int32 = -1
lb.SendMessage(win.LB_SETSEL, win.FALSE, uintptr(m))
for _, v := range indexes {
lb.SendMessage(win.LB_SETSEL, win.TRUE, uintptr(uint32(v)))
}
lb.selectedIndexesChangedPublisher.Publish()
}
func (lb *ListBox) CurrentIndexChanged() *Event {
return lb.currentIndexChangedPublisher.Event()
}
func (lb *ListBox) SelectedIndexesChanged() *Event {
return lb.selectedIndexesChangedPublisher.Event()
}
func (lb *ListBox) ItemActivated() *Event {
return lb.itemActivatedPublisher.Event()
}
func (lb *ListBox) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_MEASUREITEM:
if lb.styler == nil {
break
}
mis := (*win.MEASUREITEMSTRUCT)(unsafe.Pointer(lParam))
mis.ItemHeight = uint32(lb.styler.DefaultItemHeight())
return win.TRUE
case win.WM_DRAWITEM:
dis := (*win.DRAWITEMSTRUCT)(unsafe.Pointer(lParam))
if lb.styler == nil || dis.ItemID < 0 || dis.ItemAction != win.ODA_DRAWENTIRE {
return win.TRUE
}
lb.style.index = int(dis.ItemID)
lb.style.rc = dis.RcItem
lb.style.bounds = rectangleFromRECT(dis.RcItem)
lb.style.dpi = lb.DPI()
lb.style.state = dis.ItemState
lb.style.hwnd = lb.hWnd
lb.style.hdc = dis.HDC
lb.style.Font = lb.Font()
if dis.ItemAction == win.ODA_FOCUS {
return win.TRUE
}
var hTheme win.HTHEME
if !lb.style.highContrastActive {
if hTheme = win.OpenThemeData(lb.hWnd, syscall.StringToUTF16Ptr("Listview")); hTheme != 0 {
defer win.CloseThemeData(hTheme)
}
}
lb.style.hTheme = hTheme
if dis.ItemState&win.ODS_CHECKED != 0 {
if lb.style.highContrastActive || lb.Focused() {
lb.style.BackgroundColor = lb.themeSelectedBGColor
lb.style.TextColor = lb.themeSelectedTextColor
} else {
lb.style.BackgroundColor = lb.themeSelectedNotFocusedBGColor
lb.style.TextColor = lb.themeNormalTextColor
}
} else if int(dis.ItemID) == lb.style.hoverIndex {
if hTheme == 0 {
lb.style.BackgroundColor = lb.themeNormalBGColor
} else {
lb.style.BackgroundColor = lb.themeSelectedBGColor
}
lb.style.TextColor = lb.themeNormalTextColor
} else {
lb.style.BackgroundColor = lb.themeNormalBGColor
lb.style.TextColor = lb.themeNormalTextColor
}
if lb.themeNormalTextColor == RGB(0, 0, 0) {
lb.style.LineColor = RGB(0, 0, 0)
} else {
lb.style.LineColor = RGB(255, 255, 255)
}
lb.style.DrawBackground()
lb.styler.StyleItem(&lb.style)
defer func() {
lb.style.bounds = Rectangle{}
if lb.style.canvas != nil {
lb.style.canvas.Dispose()
lb.style.canvas = nil
}
lb.style.hdc = 0
}()
return win.TRUE
case win.WM_WINDOWPOSCHANGED:
wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam))
if wp.Flags&win.SWP_NOSIZE != 0 {
break
}
if lb.styler != nil && lb.styler.ItemHeightDependsOnWidth() {
width := lb.WidthPixels()
if width != lb.lastWidth {
lb.lastWidth = width
lb.lastWidthsMeasuredFor = make([]int, lb.model.ItemCount())
}
}
lb.ensureVisibleItemsHeightUpToDate()
case win.WM_VSCROLL:
lb.ensureVisibleItemsHeightUpToDate()
case win.WM_MOUSEWHEEL:
lb.ensureVisibleItemsHeightUpToDate()
case win.WM_LBUTTONDOWN:
lb.Invalidate()
case win.WM_MOUSEMOVE:
if !lb.trackingMouseEvent {
var tme win.TRACKMOUSEEVENT
tme.CbSize = uint32(unsafe.Sizeof(tme))
tme.DwFlags = win.TME_LEAVE
tme.HwndTrack = lb.hWnd
lb.trackingMouseEvent = win.TrackMouseEvent(&tme)
}
oldHoverIndex := lb.style.hoverIndex
result := uint32(lb.SendMessage(win.LB_ITEMFROMPOINT, 0, lParam))
if win.HIWORD(result) == 0 {
index := int(win.LOWORD(result))
var rc win.RECT
lb.SendMessage(win.LB_GETITEMRECT, uintptr(index), uintptr(unsafe.Pointer(&rc)))
lp := uint32(lParam)
x := int32(win.LOWORD(lp))
y := int32(win.HIWORD(lp))
if x >= rc.Left && x <= rc.Right && y >= rc.Top && y <= rc.Bottom {
lb.style.hoverIndex = index
win.InvalidateRect(lb.hWnd, &rc, true)
}
}
if lb.style.hoverIndex != oldHoverIndex {
if wParam&win.MK_LBUTTON != 0 {
lb.Invalidate()
} else {
lb.invalidateItem(oldHoverIndex)
lb.invalidateItem(lb.style.hoverIndex)
}
}
case win.WM_MOUSELEAVE:
lb.trackingMouseEvent = false
index := lb.style.hoverIndex
lb.style.hoverIndex = -1
lb.invalidateItem(index)
case win.WM_COMMAND:
switch win.HIWORD(uint32(wParam)) {
case win.LBN_SELCHANGE:
lb.ensureVisibleItemsHeightUpToDate()
lb.prevCurIndex = lb.CurrentIndex()
lb.currentIndexChangedPublisher.Publish()
lb.selectedIndexesChangedPublisher.Publish()
case win.LBN_DBLCLK:
lb.itemActivatedPublisher.Publish()
}
case win.WM_GETDLGCODE:
if form := ancestor(lb); form != nil {
if dlg, ok := form.(dialogish); ok {
if dlg.DefaultButton() != nil {
// If the ListBox 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 uint32(lParam)>>30 == 0 && Key(wParam) == KeyReturn && lb.CurrentIndex() > -1 {
lb.itemActivatedPublisher.Publish()
}
}
return lb.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
}
func (lb *ListBox) invalidateItem(index int) {
var rc win.RECT
lb.SendMessage(win.LB_GETITEMRECT, uintptr(index), uintptr(unsafe.Pointer(&rc)))
win.InvalidateRect(lb.hWnd, &rc, true)
}
func (lb *ListBox) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
return NewGreedyLayoutItem()
}