mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 19:50:15 +08:00
550 lines
14 KiB
Go
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)
|
|
}
|