5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 18:29:53 +08:00
wails/v2/internal/frontend/desktop/windows/winc/listview.go
reallylowest e7756e9274
chore: fix some typos in comments (#3357)
Signed-off-by: reallylowest <sunjinping@outlook.com>
2024-03-31 16:23:18 +11:00

550 lines
14 KiB
Go

//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import (
"errors"
"fmt"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
// ListItem represents an item in a ListView widget.
type ListItem interface {
Text() []string // Text returns the text of the multi-column item.
ImageIndex() int // ImageIndex is used only if SetImageList is called on the listview
}
// ListItemChecker is used for checkbox support in ListView.
type ListItemChecker interface {
Checked() bool
SetChecked(checked bool)
}
// ListItemSetter is used in OnEndLabelEdit event.
type ListItemSetter interface {
SetText(s string) // set first item in the array via LabelEdit event
}
// StringListItem is helper for basic string lists.
type StringListItem struct {
ID int
Data string
Check bool
}
func (s StringListItem) Text() []string { return []string{s.Data} }
func (s StringListItem) Checked() bool { return s.Check }
func (s StringListItem) SetChecked(checked bool) { s.Check = checked }
func (s StringListItem) ImageIndex() int { return 0 }
type ListView struct {
ControlBase
iml *ImageList
lastIndex int
cols int // count of columns
item2Handle map[ListItem]uintptr
handle2Item map[uintptr]ListItem
onEndLabelEdit EventManager
onDoubleClick EventManager
onClick EventManager
onKeyDown EventManager
onItemChanging EventManager
onItemChanged EventManager
onCheckChanged EventManager
onViewChange EventManager
onEndScroll EventManager
}
func NewListView(parent Controller) *ListView {
lv := new(ListView)
lv.InitControl("SysListView32", parent /*w32.WS_EX_CLIENTEDGE*/, 0,
w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.LVS_REPORT|w32.LVS_EDITLABELS|w32.LVS_SHOWSELALWAYS)
lv.item2Handle = make(map[ListItem]uintptr)
lv.handle2Item = make(map[uintptr]ListItem)
RegMsgHandler(lv)
lv.SetFont(DefaultFont)
lv.SetSize(200, 400)
if err := lv.SetTheme("Explorer"); err != nil {
// theme error is ignored
}
return lv
}
// FIXME: Changes the state of an item in a list-view control. Refer LVM_SETITEMSTATE message.
func (lv *ListView) setItemState(i int, state, mask uint) {
var item w32.LVITEM
item.State, item.StateMask = uint32(state), uint32(mask)
w32.SendMessage(lv.hwnd, w32.LVM_SETITEMSTATE, uintptr(i), uintptr(unsafe.Pointer(&item)))
}
func (lv *ListView) EnableSingleSelect(enable bool) {
SetStyle(lv.hwnd, enable, w32.LVS_SINGLESEL)
}
func (lv *ListView) EnableSortHeader(enable bool) {
SetStyle(lv.hwnd, enable, w32.LVS_NOSORTHEADER)
}
func (lv *ListView) EnableSortAscending(enable bool) {
SetStyle(lv.hwnd, enable, w32.LVS_SORTASCENDING)
}
func (lv *ListView) EnableEditLabels(enable bool) {
SetStyle(lv.hwnd, enable, w32.LVS_EDITLABELS)
}
func (lv *ListView) EnableFullRowSelect(enable bool) {
if enable {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_FULLROWSELECT)
} else {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_FULLROWSELECT, 0)
}
}
func (lv *ListView) EnableDoubleBuffer(enable bool) {
if enable {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_DOUBLEBUFFER)
} else {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_DOUBLEBUFFER, 0)
}
}
func (lv *ListView) EnableHotTrack(enable bool) {
if enable {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, w32.LVS_EX_TRACKSELECT)
} else {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, w32.LVS_EX_TRACKSELECT, 0)
}
}
func (lv *ListView) SetItemCount(count int) bool {
return w32.SendMessage(lv.hwnd, w32.LVM_SETITEMCOUNT, uintptr(count), 0) != 0
}
func (lv *ListView) ItemCount() int {
return int(w32.SendMessage(lv.hwnd, w32.LVM_GETITEMCOUNT, 0, 0))
}
func (lv *ListView) ItemAt(x, y int) ListItem {
hti := w32.LVHITTESTINFO{Pt: w32.POINT{int32(x), int32(y)}}
w32.SendMessage(lv.hwnd, w32.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))
return lv.findItemByIndex(int(hti.IItem))
}
func (lv *ListView) Items() (list []ListItem) {
for item := range lv.item2Handle {
list = append(list, item)
}
return list
}
func (lv *ListView) AddColumn(caption string, width int) {
var lc w32.LVCOLUMN
lc.Mask = w32.LVCF_TEXT
if width != 0 {
lc.Mask = lc.Mask | w32.LVCF_WIDTH
lc.Cx = int32(width)
}
lc.PszText = syscall.StringToUTF16Ptr(caption)
lv.insertLvColumn(&lc, lv.cols)
lv.cols++
}
// StretchLastColumn makes the last column take up all remaining horizontal
// space of the *ListView.
// The effect of this is not persistent.
func (lv *ListView) StretchLastColumn() error {
if lv.cols == 0 {
return nil
}
if w32.SendMessage(lv.hwnd, w32.LVM_SETCOLUMNWIDTH, uintptr(lv.cols-1), w32.LVSCW_AUTOSIZE_USEHEADER) == 0 {
//panic("LVM_SETCOLUMNWIDTH failed")
}
return nil
}
// CheckBoxes returns if the *TableView has check boxes.
func (lv *ListView) CheckBoxes() bool {
return w32.SendMessage(lv.hwnd, w32.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)&w32.LVS_EX_CHECKBOXES > 0
}
// SetCheckBoxes sets if the *TableView has check boxes.
func (lv *ListView) SetCheckBoxes(value bool) {
exStyle := w32.SendMessage(lv.hwnd, w32.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
oldStyle := exStyle
if value {
exStyle |= w32.LVS_EX_CHECKBOXES
} else {
exStyle &^= w32.LVS_EX_CHECKBOXES
}
if exStyle != oldStyle {
w32.SendMessage(lv.hwnd, w32.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
}
mask := w32.SendMessage(lv.hwnd, w32.LVM_GETCALLBACKMASK, 0, 0)
if value {
mask |= w32.LVIS_STATEIMAGEMASK
} else {
mask &^= w32.LVIS_STATEIMAGEMASK
}
if w32.SendMessage(lv.hwnd, w32.LVM_SETCALLBACKMASK, mask, 0) == w32.FALSE {
panic("SendMessage(LVM_SETCALLBACKMASK)")
}
}
func (lv *ListView) applyImage(lc *w32.LVITEM, imIndex int) {
if lv.iml != nil {
lc.Mask |= w32.LVIF_IMAGE
lc.IImage = int32(imIndex)
}
}
func (lv *ListView) AddItem(item ListItem) {
lv.InsertItem(item, lv.ItemCount())
}
func (lv *ListView) InsertItem(item ListItem, index int) {
text := item.Text()
li := &w32.LVITEM{
Mask: w32.LVIF_TEXT | w32.LVIF_PARAM,
PszText: syscall.StringToUTF16Ptr(text[0]),
IItem: int32(index),
}
lv.lastIndex++
ix := new(int)
*ix = lv.lastIndex
li.LParam = uintptr(*ix)
lv.handle2Item[li.LParam] = item
lv.item2Handle[item] = li.LParam
lv.applyImage(li, item.ImageIndex())
lv.insertLvItem(li)
for i := 1; i < len(text); i++ {
li.Mask = w32.LVIF_TEXT
li.PszText = syscall.StringToUTF16Ptr(text[i])
li.ISubItem = int32(i)
lv.setLvItem(li)
}
}
func (lv *ListView) UpdateItem(item ListItem) bool {
lparam, ok := lv.item2Handle[item]
if !ok {
return false
}
index := lv.findIndexByItem(item)
if index == -1 {
return false
}
text := item.Text()
li := &w32.LVITEM{
Mask: w32.LVIF_TEXT | w32.LVIF_PARAM,
PszText: syscall.StringToUTF16Ptr(text[0]),
LParam: lparam,
IItem: int32(index),
}
lv.applyImage(li, item.ImageIndex())
lv.setLvItem(li)
for i := 1; i < len(text); i++ {
li.Mask = w32.LVIF_TEXT
li.PszText = syscall.StringToUTF16Ptr(text[i])
li.ISubItem = int32(i)
lv.setLvItem(li)
}
return true
}
func (lv *ListView) insertLvColumn(lvColumn *w32.LVCOLUMN, iCol int) {
w32.SendMessage(lv.hwnd, w32.LVM_INSERTCOLUMN, uintptr(iCol), uintptr(unsafe.Pointer(lvColumn)))
}
func (lv *ListView) insertLvItem(lvItem *w32.LVITEM) {
w32.SendMessage(lv.hwnd, w32.LVM_INSERTITEM, 0, uintptr(unsafe.Pointer(lvItem)))
}
func (lv *ListView) setLvItem(lvItem *w32.LVITEM) {
w32.SendMessage(lv.hwnd, w32.LVM_SETITEM, 0, uintptr(unsafe.Pointer(lvItem)))
}
func (lv *ListView) DeleteAllItems() bool {
if w32.SendMessage(lv.hwnd, w32.LVM_DELETEALLITEMS, 0, 0) == w32.TRUE {
lv.item2Handle = make(map[ListItem]uintptr)
lv.handle2Item = make(map[uintptr]ListItem)
return true
}
return false
}
func (lv *ListView) DeleteItem(item ListItem) error {
index := lv.findIndexByItem(item)
if index == -1 {
return errors.New("item not found")
}
if w32.SendMessage(lv.hwnd, w32.LVM_DELETEITEM, uintptr(index), 0) == 0 {
return errors.New("SendMessage(TVM_DELETEITEM) failed")
}
h := lv.item2Handle[item]
delete(lv.item2Handle, item)
delete(lv.handle2Item, h)
return nil
}
func (lv *ListView) findIndexByItem(item ListItem) int {
lparam, ok := lv.item2Handle[item]
if !ok {
return -1
}
it := &w32.LVFINDINFO{
Flags: w32.LVFI_PARAM,
LParam: lparam,
}
var i int = -1
return int(w32.SendMessage(lv.hwnd, w32.LVM_FINDITEM, uintptr(i), uintptr(unsafe.Pointer(it))))
}
func (lv *ListView) findItemByIndex(i int) ListItem {
it := &w32.LVITEM{
Mask: w32.LVIF_PARAM,
IItem: int32(i),
}
if w32.SendMessage(lv.hwnd, w32.LVM_GETITEM, 0, uintptr(unsafe.Pointer(it))) == w32.TRUE {
if item, ok := lv.handle2Item[it.LParam]; ok {
return item
}
}
return nil
}
func (lv *ListView) EnsureVisible(item ListItem) bool {
if i := lv.findIndexByItem(item); i != -1 {
return w32.SendMessage(lv.hwnd, w32.LVM_ENSUREVISIBLE, uintptr(i), 1) == 0
}
return false
}
func (lv *ListView) SelectedItem() ListItem {
if items := lv.SelectedItems(); len(items) > 0 {
return items[0]
}
return nil
}
func (lv *ListView) SetSelectedItem(item ListItem) bool {
if i := lv.findIndexByItem(item); i > -1 {
lv.SetSelectedIndex(i)
return true
}
return false
}
// mask is used to set the LVITEM.Mask for ListView.GetItem which indicates which attributes you'd like to receive
// of LVITEM.
func (lv *ListView) SelectedItems() []ListItem {
var items []ListItem
var i int = -1
for {
if i = int(w32.SendMessage(lv.hwnd, w32.LVM_GETNEXTITEM, uintptr(i), uintptr(w32.LVNI_SELECTED))); i == -1 {
break
}
if item := lv.findItemByIndex(i); item != nil {
items = append(items, item)
}
}
return items
}
func (lv *ListView) SelectedCount() uint {
return uint(w32.SendMessage(lv.hwnd, w32.LVM_GETSELECTEDCOUNT, 0, 0))
}
// GetSelectedIndex first selected item index. Returns -1 if no item is selected.
func (lv *ListView) SelectedIndex() int {
var i int = -1
return int(w32.SendMessage(lv.hwnd, w32.LVM_GETNEXTITEM, uintptr(i), uintptr(w32.LVNI_SELECTED)))
}
// Set i to -1 to select all items.
func (lv *ListView) SetSelectedIndex(i int) {
lv.setItemState(i, w32.LVIS_SELECTED, w32.LVIS_SELECTED)
}
func (lv *ListView) SetImageList(imageList *ImageList) {
w32.SendMessage(lv.hwnd, w32.LVM_SETIMAGELIST, w32.LVSIL_SMALL, uintptr(imageList.Handle()))
lv.iml = imageList
}
// Event publishers
func (lv *ListView) OnEndLabelEdit() *EventManager {
return &lv.onEndLabelEdit
}
func (lv *ListView) OnDoubleClick() *EventManager {
return &lv.onDoubleClick
}
func (lv *ListView) OnClick() *EventManager {
return &lv.onClick
}
func (lv *ListView) OnKeyDown() *EventManager {
return &lv.onKeyDown
}
func (lv *ListView) OnItemChanging() *EventManager {
return &lv.onItemChanging
}
func (lv *ListView) OnItemChanged() *EventManager {
return &lv.onItemChanged
}
func (lv *ListView) OnCheckChanged() *EventManager {
return &lv.onCheckChanged
}
func (lv *ListView) OnViewChange() *EventManager {
return &lv.onViewChange
}
func (lv *ListView) OnEndScroll() *EventManager {
return &lv.onEndScroll
}
// Message processor
func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
/*case w32.WM_ERASEBKGND:
lv.StretchLastColumn()
println("case w32.WM_ERASEBKGND")
return 1*/
case w32.WM_NOTIFY:
nm := (*w32.NMHDR)(unsafe.Pointer(lparam))
code := int32(nm.Code)
switch code {
case w32.LVN_BEGINLABELEDITW:
// println("Begin label edit")
case w32.LVN_ENDLABELEDITW:
nmdi := (*w32.NMLVDISPINFO)(unsafe.Pointer(lparam))
if nmdi.Item.PszText != nil {
fmt.Println(nmdi.Item.PszText, nmdi.Item)
if item, ok := lv.handle2Item[nmdi.Item.LParam]; ok {
lv.onEndLabelEdit.Fire(NewEvent(lv,
&LabelEditEventData{Item: item,
Text: w32.UTF16PtrToString(nmdi.Item.PszText)}))
}
return w32.TRUE
}
case w32.NM_DBLCLK:
lv.onDoubleClick.Fire(NewEvent(lv, nil))
case w32.NM_CLICK:
ac := (*w32.NMITEMACTIVATE)(unsafe.Pointer(lparam))
var hti w32.LVHITTESTINFO
hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y}
w32.SendMessage(lv.hwnd, w32.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))
if hti.Flags == w32.LVHT_ONITEMSTATEICON {
if item := lv.findItemByIndex(int(hti.IItem)); item != nil {
if item, ok := item.(ListItemChecker); ok {
checked := !item.Checked()
item.SetChecked(checked)
lv.onCheckChanged.Fire(NewEvent(lv, item))
if w32.SendMessage(lv.hwnd, w32.LVM_UPDATE, uintptr(hti.IItem), 0) == w32.FALSE {
panic("SendMessage(LVM_UPDATE)")
}
}
}
}
hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y}
w32.SendMessage(lv.hwnd, w32.LVM_SUBITEMHITTEST, 0, uintptr(unsafe.Pointer(&hti)))
lv.onClick.Fire(NewEvent(lv, hti.ISubItem))
case w32.LVN_KEYDOWN:
nmkey := (*w32.NMLVKEYDOWN)(unsafe.Pointer(lparam))
if nmkey.WVKey == w32.VK_SPACE && lv.CheckBoxes() {
if item := lv.SelectedItem(); item != nil {
if item, ok := item.(ListItemChecker); ok {
checked := !item.Checked()
item.SetChecked(checked)
lv.onCheckChanged.Fire(NewEvent(lv, item))
}
index := lv.findIndexByItem(item)
if w32.SendMessage(lv.hwnd, w32.LVM_UPDATE, uintptr(index), 0) == w32.FALSE {
panic("SendMessage(LVM_UPDATE)")
}
}
}
lv.onKeyDown.Fire(NewEvent(lv, nmkey.WVKey))
key := nmkey.WVKey
w32.SendMessage(lv.Parent().Handle(), w32.WM_KEYDOWN, uintptr(key), 0)
case w32.LVN_ITEMCHANGING:
// This event also fires when listview has changed via code.
nmlv := (*w32.NMLISTVIEW)(unsafe.Pointer(lparam))
item := lv.findItemByIndex(int(nmlv.IItem))
lv.onItemChanging.Fire(NewEvent(lv, item))
case w32.LVN_ITEMCHANGED:
// This event also fires when listview has changed via code.
nmlv := (*w32.NMLISTVIEW)(unsafe.Pointer(lparam))
item := lv.findItemByIndex(int(nmlv.IItem))
lv.onItemChanged.Fire(NewEvent(lv, item))
case w32.LVN_GETDISPINFO:
nmdi := (*w32.NMLVDISPINFO)(unsafe.Pointer(lparam))
if nmdi.Item.StateMask&w32.LVIS_STATEIMAGEMASK > 0 {
if item, ok := lv.handle2Item[nmdi.Item.LParam]; ok {
if item, ok := item.(ListItemChecker); ok {
checked := item.Checked()
if checked {
nmdi.Item.State = 0x2000
} else {
nmdi.Item.State = 0x1000
}
}
}
}
lv.onViewChange.Fire(NewEvent(lv, nil))
case w32.LVN_ENDSCROLL:
lv.onEndScroll.Fire(NewEvent(lv, nil))
}
}
return w32.DefWindowProc(lv.hwnd, msg, wparam, lparam)
}