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