// Copyright 2011 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 ( "encoding/json" "fmt" "math/big" "reflect" "syscall" "time" "unsafe" "github.com/lxn/win" ) const tableViewWindowClass = `\o/ Walk_TableView_Class \o/` var ( white = win.COLORREF(RGB(255, 255, 255)) checkmark = string([]byte{0xE2, 0x9C, 0x94}) tableViewFrozenLVWndProcPtr uintptr tableViewNormalLVWndProcPtr uintptr tableViewHdrWndProcPtr uintptr ) func init() { AppendToWalkInit(func() { MustRegisterWindowClass(tableViewWindowClass) tableViewFrozenLVWndProcPtr = syscall.NewCallback(tableViewFrozenLVWndProc) tableViewNormalLVWndProcPtr = syscall.NewCallback(tableViewNormalLVWndProc) tableViewHdrWndProcPtr = syscall.NewCallback(tableViewHdrWndProc) }) } const ( tableViewCurrentIndexChangedTimerId = 1 + iota tableViewSelectedIndexesChangedTimerId ) type TableViewCfg struct { Style uint32 CustomHeaderHeight int // in native pixels? CustomRowHeight int // in native pixels? } // TableView is a model based widget for record centric, tabular data. // // TableView is implemented as a virtual mode list view to support quite large // amounts of data. type TableView struct { WidgetBase hwndFrozenLV win.HWND hwndFrozenHdr win.HWND frozenLVOrigWndProcPtr uintptr frozenHdrOrigWndProcPtr uintptr hwndNormalLV win.HWND hwndNormalHdr win.HWND normalLVOrigWndProcPtr uintptr normalHdrOrigWndProcPtr uintptr state *tableViewState columns *TableViewColumnList model TableModel providedModel interface{} itemChecker ItemChecker imageProvider ImageProvider styler CellStyler style CellStyle itemFont *Font hIml win.HIMAGELIST usingSysIml bool imageUintptr2Index map[uintptr]int32 filePath2IconIndex map[string]int32 rowsResetHandlerHandle int rowChangedHandlerHandle int rowsChangedHandlerHandle int rowsInsertedHandlerHandle int rowsRemovedHandlerHandle int sortChangedHandlerHandle int selectedIndexes []int prevIndex int currentIndex int itemIndexOfLastMouseButtonDown int hwndItemChanged win.HWND currentIndexChangedPublisher EventPublisher selectedIndexesChangedPublisher EventPublisher itemActivatedPublisher EventPublisher columnClickedPublisher IntEventPublisher columnsOrderableChangedPublisher EventPublisher columnsSizableChangedPublisher EventPublisher itemCountChangedPublisher EventPublisher publishNextSelClear bool inSetSelectedIndexes bool lastColumnStretched bool persistent bool itemStateChangedEventDelay int themeNormalBGColor Color themeNormalTextColor Color themeSelectedBGColor Color themeSelectedTextColor Color themeSelectedNotFocusedBGColor Color itemBGColor Color itemTextColor Color alternatingRowBGColor Color alternatingRowTextColor Color alternatingRowBG bool delayedCurrentIndexChangedCanceled bool sortedColumnIndex int sortOrder SortOrder formActivatingHandle int customHeaderHeight int // in native pixels? customRowHeight int // in native pixels? dpiOfPrevStretchLastColumn int scrolling bool inSetCurrentIndex bool inMouseEvent bool hasFrozenColumn bool busyStretchingLastColumn bool focused bool ignoreNowhere bool updateLVSizesNeedsSpecialCare bool scrollbarOrientation Orientation currentItemChangedPublisher EventPublisher currentItemID interface{} restoringCurrentItemOnReset bool } // NewTableView creates and returns a *TableView as child of the specified // Container. func NewTableView(parent Container) (*TableView, error) { return NewTableViewWithStyle(parent, win.LVS_SHOWSELALWAYS) } // NewTableViewWithStyle creates and returns a *TableView as child of the specified // Container and with the provided additional style bits set. func NewTableViewWithStyle(parent Container, style uint32) (*TableView, error) { return NewTableViewWithCfg(parent, &TableViewCfg{Style: style}) } // NewTableViewWithCfg creates and returns a *TableView as child of the specified // Container and with the provided additional configuration. func NewTableViewWithCfg(parent Container, cfg *TableViewCfg) (*TableView, error) { tv := &TableView{ imageUintptr2Index: make(map[uintptr]int32), filePath2IconIndex: make(map[string]int32), formActivatingHandle: -1, customHeaderHeight: cfg.CustomHeaderHeight, customRowHeight: cfg.CustomRowHeight, scrollbarOrientation: Horizontal | Vertical, restoringCurrentItemOnReset: true, } tv.columns = newTableViewColumnList(tv) if err := InitWidget( tv, parent, tableViewWindowClass, win.WS_BORDER|win.WS_VISIBLE, win.WS_EX_CONTROLPARENT); err != nil { return nil, err } succeeded := false defer func() { if !succeeded { tv.Dispose() } }() var rowHeightStyle uint32 if cfg.CustomRowHeight > 0 { rowHeightStyle = win.LVS_OWNERDRAWFIXED } if tv.hwndFrozenLV = win.CreateWindowEx( 0, syscall.StringToUTF16Ptr("SysListView32"), nil, win.WS_CHILD|win.WS_CLIPSIBLINGS|win.WS_TABSTOP|win.WS_VISIBLE|win.LVS_OWNERDATA|win.LVS_REPORT|cfg.Style|rowHeightStyle, win.CW_USEDEFAULT, win.CW_USEDEFAULT, win.CW_USEDEFAULT, win.CW_USEDEFAULT, tv.hWnd, 0, 0, nil, ); tv.hwndFrozenLV == 0 { return nil, newError("creating frozen lv failed") } tv.frozenLVOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndFrozenLV, win.GWLP_WNDPROC, tableViewFrozenLVWndProcPtr) if tv.frozenLVOrigWndProcPtr == 0 { return nil, lastError("SetWindowLongPtr") } tv.hwndFrozenHdr = win.HWND(win.SendMessage(tv.hwndFrozenLV, win.LVM_GETHEADER, 0, 0)) tv.frozenHdrOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndFrozenHdr, win.GWLP_WNDPROC, tableViewHdrWndProcPtr) if tv.frozenHdrOrigWndProcPtr == 0 { return nil, lastError("SetWindowLongPtr") } if tv.hwndNormalLV = win.CreateWindowEx( 0, syscall.StringToUTF16Ptr("SysListView32"), nil, win.WS_CHILD|win.WS_CLIPSIBLINGS|win.WS_TABSTOP|win.WS_VISIBLE|win.LVS_OWNERDATA|win.LVS_REPORT|cfg.Style|rowHeightStyle, win.CW_USEDEFAULT, win.CW_USEDEFAULT, win.CW_USEDEFAULT, win.CW_USEDEFAULT, tv.hWnd, 0, 0, nil, ); tv.hwndNormalLV == 0 { return nil, newError("creating normal lv failed") } tv.normalLVOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndNormalLV, win.GWLP_WNDPROC, tableViewNormalLVWndProcPtr) if tv.normalLVOrigWndProcPtr == 0 { return nil, lastError("SetWindowLongPtr") } tv.hwndNormalHdr = win.HWND(win.SendMessage(tv.hwndNormalLV, win.LVM_GETHEADER, 0, 0)) tv.normalHdrOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndNormalHdr, win.GWLP_WNDPROC, tableViewHdrWndProcPtr) if tv.normalHdrOrigWndProcPtr == 0 { return nil, lastError("SetWindowLongPtr") } tv.SetPersistent(true) exStyle := win.SendMessage(tv.hwndFrozenLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) exStyle |= win.LVS_EX_DOUBLEBUFFER | win.LVS_EX_FULLROWSELECT | win.LVS_EX_HEADERDRAGDROP | win.LVS_EX_LABELTIP | win.LVS_EX_SUBITEMIMAGES win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) if hr := win.SetWindowTheme(tv.hwndFrozenLV, syscall.StringToUTF16Ptr("Explorer"), nil); win.FAILED(hr) { return nil, errorFromHRESULT("SetWindowTheme", hr) } if hr := win.SetWindowTheme(tv.hwndNormalLV, syscall.StringToUTF16Ptr("Explorer"), nil); win.FAILED(hr) { return nil, errorFromHRESULT("SetWindowTheme", hr) } win.SendMessage(tv.hwndFrozenLV, win.WM_CHANGEUISTATE, uintptr(win.MAKELONG(win.UIS_SET, win.UISF_HIDEFOCUS)), 0) win.SendMessage(tv.hwndNormalLV, win.WM_CHANGEUISTATE, uintptr(win.MAKELONG(win.UIS_SET, win.UISF_HIDEFOCUS)), 0) tv.group.toolTip.addTool(tv.hwndFrozenHdr, false) tv.group.toolTip.addTool(tv.hwndNormalHdr, false) tv.applyFont(parent.Font()) tv.style.dpi = tv.DPI() tv.ApplySysColors() tv.currentIndex = -1 tv.GraphicsEffects().Add(InteractionEffect) tv.GraphicsEffects().Add(FocusEffect) tv.MustRegisterProperty("ColumnsOrderable", NewBoolProperty( func() bool { return tv.ColumnsOrderable() }, func(b bool) error { tv.SetColumnsOrderable(b) return nil }, tv.columnsOrderableChangedPublisher.Event())) tv.MustRegisterProperty("ColumnsSizable", NewBoolProperty( func() bool { return tv.ColumnsSizable() }, func(b bool) error { return tv.SetColumnsSizable(b) }, tv.columnsSizableChangedPublisher.Event())) tv.MustRegisterProperty("CurrentIndex", NewProperty( func() interface{} { return tv.CurrentIndex() }, func(v interface{}) error { return tv.SetCurrentIndex(assertIntOr(v, -1)) }, tv.CurrentIndexChanged())) tv.MustRegisterProperty("CurrentItem", NewReadOnlyProperty( func() interface{} { if i := tv.CurrentIndex(); i > -1 { if rm, ok := tv.providedModel.(reflectModel); ok { return reflect.ValueOf(rm.Items()).Index(i).Interface() } } return nil }, tv.CurrentIndexChanged())) tv.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty( func() bool { return tv.CurrentIndex() != -1 }, tv.CurrentIndexChanged())) tv.MustRegisterProperty("ItemCount", NewReadOnlyProperty( func() interface{} { if tv.model == nil { return 0 } return tv.model.RowCount() }, tv.itemCountChangedPublisher.Event())) tv.MustRegisterProperty("SelectedCount", NewReadOnlyProperty( func() interface{} { return len(tv.selectedIndexes) }, tv.SelectedIndexesChanged())) succeeded = true return tv, nil } func (tv *TableView) asTableView() *TableView { return tv } // Dispose releases the operating system resources, associated with the // *TableView. func (tv *TableView) Dispose() { tv.columns.unsetColumnsTV() tv.disposeImageListAndCaches() if tv.hWnd != 0 { if !win.KillTimer(tv.hWnd, tableViewCurrentIndexChangedTimerId) { lastError("KillTimer") } if !win.KillTimer(tv.hWnd, tableViewSelectedIndexesChangedTimerId) { lastError("KillTimer") } } if tv.hwndFrozenLV != 0 { tv.group.toolTip.removeTool(tv.hwndFrozenHdr) win.DestroyWindow(tv.hwndFrozenLV) tv.hwndFrozenLV = 0 } if tv.hwndNormalLV != 0 { tv.group.toolTip.removeTool(tv.hwndNormalHdr) win.DestroyWindow(tv.hwndNormalLV) tv.hwndNormalLV = 0 } if tv.formActivatingHandle > -1 { if form := tv.Form(); form != nil { form.Activating().Detach(tv.formActivatingHandle) } tv.formActivatingHandle = -1 } tv.WidgetBase.Dispose() } func (tv *TableView) applyEnabled(enabled bool) { tv.WidgetBase.applyEnabled(enabled) win.EnableWindow(tv.hwndFrozenLV, enabled) win.EnableWindow(tv.hwndNormalLV, enabled) } func (tv *TableView) applyFont(font *Font) { if tv.customHeaderHeight > 0 || tv.customRowHeight > 0 { return } tv.WidgetBase.applyFont(font) hFont := uintptr(font.handleForDPI(tv.DPI())) win.SendMessage(tv.hwndFrozenLV, win.WM_SETFONT, hFont, 0) win.SendMessage(tv.hwndNormalLV, win.WM_SETFONT, hFont, 0) } func (tv *TableView) ApplyDPI(dpi int) { tv.style.dpi = dpi if tv.style.canvas != nil { tv.style.canvas.dpi = dpi } tv.WidgetBase.ApplyDPI(dpi) for _, column := range tv.columns.items { column.update() } if tv.hIml != 0 { tv.disposeImageListAndCaches() if bmp, err := NewBitmapForDPI(SizeFrom96DPI(Size{16, 16}, dpi), dpi); err == nil { tv.applyImageListForImage(bmp) bmp.Dispose() } } } func (tv *TableView) ApplySysColors() { tv.WidgetBase.ApplySysColors() // As some combinations of property and state may be invalid for any theme, // we set some defaults here. tv.themeNormalBGColor = Color(win.GetSysColor(win.COLOR_WINDOW)) tv.themeNormalTextColor = Color(win.GetSysColor(win.COLOR_WINDOWTEXT)) tv.themeSelectedBGColor = tv.themeNormalBGColor tv.themeSelectedTextColor = tv.themeNormalTextColor tv.themeSelectedNotFocusedBGColor = tv.themeNormalBGColor tv.alternatingRowBGColor = Color(win.GetSysColor(win.COLOR_BTNFACE)) tv.alternatingRowTextColor = Color(win.GetSysColor(win.COLOR_BTNTEXT)) type item struct { stateID int32 propertyID int32 color *Color } getThemeColor := func(theme win.HTHEME, partId int32, items []item) { for _, item := range items { var c win.COLORREF if result := win.GetThemeColor(theme, partId, item.stateID, item.propertyID, &c); !win.FAILED(result) { (*item.color) = Color(c) } } } if hThemeListView := win.OpenThemeData(tv.hwndNormalLV, syscall.StringToUTF16Ptr("Listview")); hThemeListView != 0 { defer win.CloseThemeData(hThemeListView) getThemeColor(hThemeListView, win.LVP_LISTITEM, []item{ {win.LISS_NORMAL, win.TMT_FILLCOLOR, &tv.themeNormalBGColor}, {win.LISS_NORMAL, win.TMT_TEXTCOLOR, &tv.themeNormalTextColor}, {win.LISS_SELECTED, win.TMT_FILLCOLOR, &tv.themeSelectedBGColor}, {win.LISS_SELECTED, win.TMT_TEXTCOLOR, &tv.themeSelectedTextColor}, {win.LISS_SELECTEDNOTFOCUS, win.TMT_FILLCOLOR, &tv.themeSelectedNotFocusedBGColor}, }) } else { // The others already have been retrieved above. tv.themeSelectedBGColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHT)) tv.themeSelectedTextColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHTTEXT)) tv.themeSelectedNotFocusedBGColor = Color(win.GetSysColor(win.COLOR_BTNFACE)) } if hThemeButton := win.OpenThemeData(tv.hwndNormalLV, syscall.StringToUTF16Ptr("BUTTON")); hThemeButton != 0 { defer win.CloseThemeData(hThemeButton) getThemeColor(hThemeButton, win.BP_PUSHBUTTON, []item{ {win.PBS_NORMAL, win.TMT_FILLCOLOR, &tv.alternatingRowBGColor}, {win.PBS_NORMAL, win.TMT_TEXTCOLOR, &tv.alternatingRowTextColor}, }) } win.SendMessage(tv.hwndNormalLV, win.LVM_SETBKCOLOR, 0, uintptr(tv.themeNormalBGColor)) win.SendMessage(tv.hwndFrozenLV, win.LVM_SETBKCOLOR, 0, uintptr(tv.themeNormalBGColor)) } // ColumnsOrderable returns if the user can reorder columns by dragging and // dropping column headers. func (tv *TableView) ColumnsOrderable() bool { exStyle := win.SendMessage(tv.hwndNormalLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) return exStyle&win.LVS_EX_HEADERDRAGDROP > 0 } // SetColumnsOrderable sets if the user can reorder columns by dragging and // dropping column headers. func (tv *TableView) SetColumnsOrderable(enabled bool) { var hwnd win.HWND if tv.hasFrozenColumn { hwnd = tv.hwndFrozenLV } else { hwnd = tv.hwndNormalLV } exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) if enabled { exStyle |= win.LVS_EX_HEADERDRAGDROP } else { exStyle &^= win.LVS_EX_HEADERDRAGDROP } win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) tv.columnsOrderableChangedPublisher.Publish() } // ColumnsSizable returns if the user can change column widths by dragging // dividers in the header. func (tv *TableView) ColumnsSizable() bool { style := win.GetWindowLong(tv.hwndNormalHdr, win.GWL_STYLE) return style&win.HDS_NOSIZING == 0 } // SetColumnsSizable sets if the user can change column widths by dragging // dividers in the header. func (tv *TableView) SetColumnsSizable(b bool) error { updateStyle := func(headerHWnd win.HWND) error { style := win.GetWindowLong(headerHWnd, win.GWL_STYLE) if b { style &^= win.HDS_NOSIZING } else { style |= win.HDS_NOSIZING } if 0 == win.SetWindowLong(headerHWnd, win.GWL_STYLE, style) { return lastError("SetWindowLong(GWL_STYLE)") } return nil } if err := updateStyle(tv.hwndFrozenHdr); err != nil { return err } if err := updateStyle(tv.hwndNormalHdr); err != nil { return err } tv.columnsSizableChangedPublisher.Publish() return nil } // ContextMenuLocation returns selected item position in screen coordinates in native pixels. func (tv *TableView) ContextMenuLocation() Point { idx := win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTIONMARK, 0, 0) rc := win.RECT{Left: win.LVIR_BOUNDS} if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_GETITEMRECT, idx, uintptr(unsafe.Pointer(&rc))) { return tv.WidgetBase.ContextMenuLocation() } var pt win.POINT if tv.RightToLeftReading() { pt.X = rc.Right } else { pt.X = rc.Left } pt.X = rc.Bottom windowTrimToClientBounds(tv.hwndNormalLV, &pt) win.ClientToScreen(tv.hwndNormalLV, &pt) return pointPixelsFromPOINT(pt) } // SortableByHeaderClick returns if the user can change sorting by clicking the header. func (tv *TableView) SortableByHeaderClick() bool { return !hasWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_NOSORTHEADER) || !hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_NOSORTHEADER) } // HeaderHidden returns whether the column header is hidden. func (tv *TableView) HeaderHidden() bool { style := win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE) return style&win.LVS_NOCOLUMNHEADER != 0 } // SetHeaderHidden sets whether the column header is hidden. func (tv *TableView) SetHeaderHidden(hidden bool) error { if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_NOCOLUMNHEADER, hidden); err != nil { return err } return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_NOCOLUMNHEADER, hidden) } // AlternatingRowBG returns the alternating row background. func (tv *TableView) AlternatingRowBG() bool { return tv.alternatingRowBG } // SetAlternatingRowBG sets the alternating row background. func (tv *TableView) SetAlternatingRowBG(enabled bool) { tv.alternatingRowBG = enabled tv.Invalidate() } // Gridlines returns if the rows are separated by grid lines. func (tv *TableView) Gridlines() bool { exStyle := win.SendMessage(tv.hwndNormalLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) return exStyle&win.LVS_EX_GRIDLINES > 0 } // SetGridlines sets if the rows are separated by grid lines. func (tv *TableView) SetGridlines(enabled bool) { var hwnd win.HWND if tv.hasFrozenColumn { hwnd = tv.hwndFrozenLV } else { hwnd = tv.hwndNormalLV } exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) if enabled { exStyle |= win.LVS_EX_GRIDLINES } else { exStyle &^= win.LVS_EX_GRIDLINES } win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) } // Columns returns the list of columns. func (tv *TableView) Columns() *TableViewColumnList { return tv.columns } // VisibleColumnsInDisplayOrder returns a slice of visible columns in display // order. func (tv *TableView) VisibleColumnsInDisplayOrder() []*TableViewColumn { visibleCols := tv.visibleColumns() indices := make([]int32, len(visibleCols)) frozenCount := tv.visibleFrozenColumnCount() normalCount := len(visibleCols) - frozenCount if frozenCount > 0 { if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(frozenCount), uintptr(unsafe.Pointer(&indices[0]))) { newError("LVM_GETCOLUMNORDERARRAY") return nil } } if normalCount > 0 { if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(normalCount), uintptr(unsafe.Pointer(&indices[frozenCount]))) { newError("LVM_GETCOLUMNORDERARRAY") return nil } } orderedCols := make([]*TableViewColumn, len(visibleCols)) for i, j := range indices { if i >= frozenCount { j += int32(frozenCount) } orderedCols[i] = visibleCols[j] } return orderedCols } // RowsPerPage returns the number of fully visible rows. func (tv *TableView) RowsPerPage() int { return int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOUNTPERPAGE, 0, 0)) } func (tv *TableView) Invalidate() error { win.InvalidateRect(tv.hwndFrozenLV, nil, true) win.InvalidateRect(tv.hwndNormalLV, nil, true) return tv.WidgetBase.Invalidate() } func (tv *TableView) redrawItems() { first := win.SendMessage(tv.hwndNormalLV, win.LVM_GETTOPINDEX, 0, 0) last := first + win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOUNTPERPAGE, 0, 0) + 1 win.SendMessage(tv.hwndFrozenLV, win.LVM_REDRAWITEMS, first, last) win.SendMessage(tv.hwndNormalLV, win.LVM_REDRAWITEMS, first, last) } // UpdateItem ensures the item at index will be redrawn. // // If the model supports sorting, it will be resorted. func (tv *TableView) UpdateItem(index int) error { if s, ok := tv.model.(Sorter); ok { if err := s.Sort(s.SortedColumn(), s.SortOrder()); err != nil { return err } } else { if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_UPDATE, uintptr(index), 0) { return newError("LVM_UPDATE") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_UPDATE, uintptr(index), 0) { return newError("LVM_UPDATE") } } return nil } func (tv *TableView) attachModel() { restoreCurrentItemOrFallbackToFirst := func(ip IDProvider) { if tv.itemStateChangedEventDelay == 0 { defer tv.currentItemChangedPublisher.Publish() } else { if 0 == win.SetTimer( tv.hWnd, tableViewCurrentIndexChangedTimerId, uint32(tv.itemStateChangedEventDelay), 0, ) { lastError("SetTimer") } } count := tv.model.RowCount() for i := 0; i < count; i++ { if ip.ID(i) == tv.currentItemID { tv.SetCurrentIndex(i) return } } tv.SetCurrentIndex(0) } tv.rowsResetHandlerHandle = tv.model.RowsReset().Attach(func() { tv.setItemCount() if ip, ok := tv.providedModel.(IDProvider); ok && tv.restoringCurrentItemOnReset { if _, ok := tv.model.(Sorter); !ok { restoreCurrentItemOrFallbackToFirst(ip) } } else { tv.SetCurrentIndex(-1) } tv.itemCountChangedPublisher.Publish() }) tv.rowChangedHandlerHandle = tv.model.RowChanged().Attach(func(row int) { tv.UpdateItem(row) }) tv.rowsChangedHandlerHandle = tv.model.RowsChanged().Attach(func(from, to int) { if s, ok := tv.model.(Sorter); ok { s.Sort(s.SortedColumn(), s.SortOrder()) } else { first, last := uintptr(from), uintptr(to) win.SendMessage(tv.hwndFrozenLV, win.LVM_REDRAWITEMS, first, last) win.SendMessage(tv.hwndNormalLV, win.LVM_REDRAWITEMS, first, last) } }) tv.rowsInsertedHandlerHandle = tv.model.RowsInserted().Attach(func(from, to int) { i := tv.currentIndex tv.setItemCount() if from <= i { i += 1 + to - from tv.SetCurrentIndex(i) } tv.itemCountChangedPublisher.Publish() }) tv.rowsRemovedHandlerHandle = tv.model.RowsRemoved().Attach(func(from, to int) { i := tv.currentIndex tv.setItemCount() index := i if from <= i && i <= to { index = -1 } else if from < i { index -= 1 + to - from } if index != i { tv.SetCurrentIndex(index) } tv.itemCountChangedPublisher.Publish() }) if sorter, ok := tv.model.(Sorter); ok { tv.sortChangedHandlerHandle = sorter.SortChanged().Attach(func() { if ip, ok := tv.providedModel.(IDProvider); ok && tv.restoringCurrentItemOnReset { restoreCurrentItemOrFallbackToFirst(ip) } col := sorter.SortedColumn() tv.setSortIcon(col, sorter.SortOrder()) tv.redrawItems() }) } } func (tv *TableView) detachModel() { tv.model.RowsReset().Detach(tv.rowsResetHandlerHandle) tv.model.RowChanged().Detach(tv.rowChangedHandlerHandle) tv.model.RowsInserted().Detach(tv.rowsInsertedHandlerHandle) tv.model.RowsRemoved().Detach(tv.rowsRemovedHandlerHandle) if sorter, ok := tv.model.(Sorter); ok { sorter.SortChanged().Detach(tv.sortChangedHandlerHandle) } } // ItemCountChanged returns the event that is published when the number of items // in the model of the TableView changed. func (tv *TableView) ItemCountChanged() *Event { return tv.itemCountChangedPublisher.Event() } // Model returns the model of the TableView. func (tv *TableView) Model() interface{} { return tv.providedModel } // SetModel sets the model of the TableView. // // It is required that mdl either implements walk.TableModel, // walk.ReflectTableModel or be a slice of pointers to struct or a // []map[string]interface{}. A walk.TableModel implementation must also // implement walk.Sorter to support sorting, all other options get sorting for // free. To support item check boxes and icons, mdl must implement // walk.ItemChecker and walk.ImageProvider, respectively. On-demand model // population for a walk.ReflectTableModel or slice requires mdl to implement // walk.Populator. func (tv *TableView) SetModel(mdl interface{}) error { model, ok := mdl.(TableModel) if !ok && mdl != nil { var err error if model, err = newReflectTableModel(mdl); err != nil { if model, err = newMapTableModel(mdl); err != nil { return err } } } tv.SetSuspended(true) defer tv.SetSuspended(false) if tv.model != nil { tv.detachModel() tv.disposeImageListAndCaches() } oldProvidedModelStyler, _ := tv.providedModel.(CellStyler) if styler, ok := mdl.(CellStyler); ok || tv.styler == oldProvidedModelStyler { tv.styler = styler } tv.providedModel = mdl tv.model = model tv.itemChecker, _ = model.(ItemChecker) tv.imageProvider, _ = model.(ImageProvider) if model != nil { tv.attachModel() if dms, ok := model.(dataMembersSetter); ok { // FIXME: This depends on columns to be initialized before // calling this method. dataMembers := make([]string, len(tv.columns.items)) for i, col := range tv.columns.items { dataMembers[i] = col.DataMemberEffective() } dms.setDataMembers(dataMembers) } if lfs, ok := model.(lessFuncsSetter); ok { lessFuncs := make([]func(i, j int) bool, tv.columns.Len()) for i, c := range tv.columns.items { lessFuncs[i] = c.lessFunc } lfs.setLessFuncs(lessFuncs) } if sorter, ok := tv.model.(Sorter); ok { if tv.sortedColumnIndex >= tv.visibleColumnCount() { tv.sortedColumnIndex = maxi(-1, mini(0, tv.visibleColumnCount()-1)) tv.sortOrder = SortAscending } sorter.Sort(tv.sortedColumnIndex, tv.sortOrder) } } tv.SetCurrentIndex(-1) tv.setItemCount() tv.itemCountChangedPublisher.Publish() return nil } // TableModel returns the TableModel of the TableView. func (tv *TableView) TableModel() TableModel { return tv.model } // ItemChecker returns the ItemChecker of the TableView. func (tv *TableView) ItemChecker() ItemChecker { return tv.itemChecker } // SetItemChecker sets the ItemChecker of the TableView. func (tv *TableView) SetItemChecker(itemChecker ItemChecker) { tv.itemChecker = itemChecker } // CellStyler returns the CellStyler of the TableView. func (tv *TableView) CellStyler() CellStyler { return tv.styler } // SetCellStyler sets the CellStyler of the TableView. func (tv *TableView) SetCellStyler(styler CellStyler) { tv.styler = styler } func (tv *TableView) setItemCount() error { var count int if tv.model != nil { count = tv.model.RowCount() } if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMCOUNT, uintptr(count), win.LVSICF_NOINVALIDATEALL|win.LVSICF_NOSCROLL) { return newError("SendMessage(LVM_SETITEMCOUNT)") } if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMCOUNT, uintptr(count), win.LVSICF_NOINVALIDATEALL|win.LVSICF_NOSCROLL) { return newError("SendMessage(LVM_SETITEMCOUNT)") } return nil } // CheckBoxes returns if the *TableView has check boxes. func (tv *TableView) CheckBoxes() bool { var hwnd win.HWND if tv.hasFrozenColumn { hwnd = tv.hwndFrozenLV } else { hwnd = tv.hwndNormalLV } return win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)&win.LVS_EX_CHECKBOXES > 0 } // SetCheckBoxes sets if the *TableView has check boxes. func (tv *TableView) SetCheckBoxes(checkBoxes bool) { var hwnd, hwndOther win.HWND if tv.hasFrozenColumn { hwnd, hwndOther = tv.hwndFrozenLV, tv.hwndNormalLV } else { hwnd, hwndOther = tv.hwndNormalLV, tv.hwndFrozenLV } exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) oldStyle := exStyle if checkBoxes { exStyle |= win.LVS_EX_CHECKBOXES } else { exStyle &^= win.LVS_EX_CHECKBOXES } if exStyle != oldStyle { win.SendMessage(hwnd, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle) } win.SendMessage(hwndOther, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle&^win.LVS_EX_CHECKBOXES) mask := win.SendMessage(hwnd, win.LVM_GETCALLBACKMASK, 0, 0) if checkBoxes { mask |= win.LVIS_STATEIMAGEMASK } else { mask &^= win.LVIS_STATEIMAGEMASK } if win.FALSE == win.SendMessage(hwnd, win.LVM_SETCALLBACKMASK, mask, 0) { newError("SendMessage(LVM_SETCALLBACKMASK)") } } func (tv *TableView) fromLVColIdx(frozen bool, index int32) int { var idx int32 for i, tvc := range tv.columns.items { if frozen == tvc.frozen && tvc.visible { if idx == index { return i } idx++ } } return -1 } func (tv *TableView) toLVColIdx(index int) int32 { var idx int32 for i, tvc := range tv.columns.items { if tvc.visible { if i == index { return idx } idx++ } } return -1 } func (tv *TableView) visibleFrozenColumnCount() int { var count int for _, tvc := range tv.columns.items { if tvc.frozen && tvc.visible { count++ } } return count } func (tv *TableView) visibleColumnCount() int { var count int for _, tvc := range tv.columns.items { if tvc.visible { count++ } } return count } func (tv *TableView) visibleColumns() []*TableViewColumn { var cols []*TableViewColumn for _, tvc := range tv.columns.items { if tvc.visible { cols = append(cols, tvc) } } return cols } /*func (tv *TableView) selectedColumnIndex() int { return tv.fromLVColIdx(tv.SendMessage(LVM_GETSELECTEDCOLUMN, 0, 0)) }*/ // func (tv *TableView) setSelectedColumnIndex(index int) { // tv.SendMessage(win.LVM_SETSELECTEDCOLUMN, uintptr(tv.toLVColIdx(index)), 0) // } func (tv *TableView) setSortIcon(index int, order SortOrder) error { idx := int(tv.toLVColIdx(index)) frozenCount := tv.visibleFrozenColumnCount() for i, col := range tv.visibleColumns() { item := win.HDITEM{ Mask: win.HDI_FORMAT, } var headerHwnd win.HWND var offset int if col.frozen { headerHwnd = tv.hwndFrozenHdr } else { headerHwnd = tv.hwndNormalHdr offset = -frozenCount } iPtr := uintptr(offset + i) itemPtr := uintptr(unsafe.Pointer(&item)) if win.SendMessage(headerHwnd, win.HDM_GETITEM, iPtr, itemPtr) == 0 { return newError("SendMessage(HDM_GETITEM)") } if i == idx { switch order { case SortAscending: item.Fmt &^= win.HDF_SORTDOWN item.Fmt |= win.HDF_SORTUP case SortDescending: item.Fmt &^= win.HDF_SORTUP item.Fmt |= win.HDF_SORTDOWN } } else { item.Fmt &^= win.HDF_SORTDOWN | win.HDF_SORTUP } if win.SendMessage(headerHwnd, win.HDM_SETITEM, iPtr, itemPtr) == 0 { return newError("SendMessage(HDM_SETITEM)") } } return nil } // ColumnClicked returns the event that is published after a column header was // clicked. func (tv *TableView) ColumnClicked() *IntEvent { return tv.columnClickedPublisher.Event() } // ItemActivated returns the event that is published after an item was // activated. // // An item is activated when it is double clicked or the enter key is pressed // when the item is selected. func (tv *TableView) ItemActivated() *Event { return tv.itemActivatedPublisher.Event() } // RestoringCurrentItemOnReset returns whether the TableView after its model // has been reset should attempt to restore CurrentIndex to the item that was // current before the reset. // // For this to work, the model must implement the IDProvider interface. func (tv *TableView) RestoringCurrentItemOnReset() bool { return tv.restoringCurrentItemOnReset } // SetRestoringCurrentItemOnReset sets whether the TableView after its model // has been reset should attempt to restore CurrentIndex to the item that was // current before the reset. // // For this to work, the model must implement the IDProvider interface. func (tv *TableView) SetRestoringCurrentItemOnReset(restoring bool) { tv.restoringCurrentItemOnReset = restoring } // CurrentItemChanged returns the event that is published after the current // item has changed. // // For this to work, the model must implement the IDProvider interface. func (tv *TableView) CurrentItemChanged() *Event { return tv.currentItemChangedPublisher.Event() } // CurrentIndex returns the index of the current item, or -1 if there is no // current item. func (tv *TableView) CurrentIndex() int { return tv.currentIndex } // SetCurrentIndex sets the index of the current item. // // Call this with a value of -1 to have no current item. func (tv *TableView) SetCurrentIndex(index int) error { if tv.inSetCurrentIndex { return nil } tv.inSetCurrentIndex = true defer func() { tv.inSetCurrentIndex = false }() var lvi win.LVITEM lvi.StateMask = win.LVIS_FOCUSED | win.LVIS_SELECTED if tv.MultiSelection() { if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, ^uintptr(0), uintptr(unsafe.Pointer(&lvi))) { return newError("SendMessage(LVM_SETITEMSTATE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, ^uintptr(0), uintptr(unsafe.Pointer(&lvi))) { return newError("SendMessage(LVM_SETITEMSTATE)") } } if index > -1 { lvi.State = win.LVIS_FOCUSED | win.LVIS_SELECTED } if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, uintptr(index), uintptr(unsafe.Pointer(&lvi))) { return newError("SendMessage(LVM_SETITEMSTATE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, uintptr(index), uintptr(unsafe.Pointer(&lvi))) { return newError("SendMessage(LVM_SETITEMSTATE)") } if index > -1 { if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) { return newError("SendMessage(LVM_ENSUREVISIBLE)") } // Windows bug? Sometimes a second LVM_ENSUREVISIBLE is required. if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) { return newError("SendMessage(LVM_ENSUREVISIBLE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) { return newError("SendMessage(LVM_ENSUREVISIBLE)") } // Windows bug? Sometimes a second LVM_ENSUREVISIBLE is required. if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) { return newError("SendMessage(LVM_ENSUREVISIBLE)") } if ip, ok := tv.providedModel.(IDProvider); ok && tv.restoringCurrentItemOnReset { if id := ip.ID(index); id != tv.currentItemID { tv.currentItemID = id if tv.itemStateChangedEventDelay == 0 { defer tv.currentItemChangedPublisher.Publish() } } } } else { tv.currentItemID = nil if tv.itemStateChangedEventDelay == 0 { defer tv.currentItemChangedPublisher.Publish() } } tv.currentIndex = index if index == -1 || tv.itemStateChangedEventDelay == 0 { tv.currentIndexChangedPublisher.Publish() } if tv.MultiSelection() { tv.updateSelectedIndexes() } return nil } // CurrentIndexChanged is the event that is published after CurrentIndex has // changed. func (tv *TableView) CurrentIndexChanged() *Event { return tv.currentIndexChangedPublisher.Event() } // IndexAt returns the item index at coordinates x, y of the // TableView or -1, if that point is not inside any item. func (tv *TableView) IndexAt(x, y int) int { var hti win.LVHITTESTINFO var rc win.RECT if !win.GetWindowRect(tv.hwndFrozenLV, &rc) { return -1 } var hwnd win.HWND if x < int(rc.Right-rc.Left) { hwnd = tv.hwndFrozenLV } else { hwnd = tv.hwndNormalLV } hti.Pt.X = int32(x) hti.Pt.Y = int32(y) win.SendMessage(hwnd, win.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) return int(hti.IItem) } // ItemVisible returns whether the item at position index is visible. func (tv *TableView) ItemVisible(index int) bool { return 0 != win.SendMessage(tv.hwndNormalLV, win.LVM_ISITEMVISIBLE, uintptr(index), 0) } // EnsureItemVisible ensures the item at position index is visible, scrolling if necessary. func (tv *TableView) EnsureItemVisible(index int) { win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), 0) } // SelectionHiddenWithoutFocus returns whether selection indicators are hidden // when the TableView does not have the keyboard input focus. func (tv *TableView) SelectionHiddenWithoutFocus() bool { style := uint(win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE)) if style == 0 { lastError("GetWindowLong") return false } return style&win.LVS_SHOWSELALWAYS == 0 } // SetSelectionHiddenWithoutFocus sets whether selection indicators are visible when the TableView does not have the keyboard input focus. func (tv *TableView) SetSelectionHiddenWithoutFocus(hidden bool) error { if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_SHOWSELALWAYS, !hidden); err != nil { return err } return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_SHOWSELALWAYS, !hidden) } // MultiSelection returns whether multiple items can be selected at once. // // By default only a single item can be selected at once. func (tv *TableView) MultiSelection() bool { style := uint(win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE)) if style == 0 { lastError("GetWindowLong") return false } return style&win.LVS_SINGLESEL == 0 } // SetMultiSelection sets whether multiple items can be selected at once. func (tv *TableView) SetMultiSelection(multiSel bool) error { if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_SINGLESEL, !multiSel); err != nil { return err } return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_SINGLESEL, !multiSel) } // SelectedIndexes returns the indexes of the currently selected items. func (tv *TableView) SelectedIndexes() []int { indexes := make([]int, len(tv.selectedIndexes)) for i, j := range tv.selectedIndexes { indexes[i] = j } return indexes } // SetSelectedIndexes sets the indexes of the currently selected items. func (tv *TableView) SetSelectedIndexes(indexes []int) error { tv.inSetSelectedIndexes = true defer func() { tv.inSetSelectedIndexes = false tv.publishSelectedIndexesChanged() }() lvi := &win.LVITEM{StateMask: win.LVIS_FOCUSED | win.LVIS_SELECTED} lp := uintptr(unsafe.Pointer(lvi)) if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, ^uintptr(0), lp) { return newError("SendMessage(LVM_SETITEMSTATE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, ^uintptr(0), lp) { return newError("SendMessage(LVM_SETITEMSTATE)") } selectAll := false lvi.State = win.LVIS_FOCUSED | win.LVIS_SELECTED for _, i := range indexes { val := uintptr(i) if i == -1 { selectAll = true val = ^uintptr(0) } if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, val, lp) && i != -1 { return newError("SendMessage(LVM_SETITEMSTATE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, val, lp) && i != -1 { return newError("SendMessage(LVM_SETITEMSTATE)") } } if !selectAll { idxs := make([]int, len(indexes)) for i, j := range indexes { idxs[i] = j } tv.selectedIndexes = idxs } else { count := int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTEDCOUNT, 0, 0)) idxs := make([]int, count) for i := range idxs { idxs[i] = i } tv.selectedIndexes = idxs } return nil } func (tv *TableView) updateSelectedIndexes() { count := int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTEDCOUNT, 0, 0)) indexes := make([]int, count) j := -1 for i := 0; i < count; i++ { j = int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETNEXTITEM, uintptr(j), win.LVNI_SELECTED)) indexes[i] = j } changed := len(indexes) != len(tv.selectedIndexes) if !changed { for i := 0; i < len(indexes); i++ { if indexes[i] != tv.selectedIndexes[i] { changed = true break } } } if changed { tv.selectedIndexes = indexes tv.publishSelectedIndexesChanged() } } func (tv *TableView) copySelectedIndexes(hwndTo, hwndFrom win.HWND) error { count := int(win.SendMessage(hwndFrom, win.LVM_GETSELECTEDCOUNT, 0, 0)) lvi := &win.LVITEM{StateMask: win.LVIS_FOCUSED | win.LVIS_SELECTED} lp := uintptr(unsafe.Pointer(lvi)) if win.FALSE == win.SendMessage(hwndTo, win.LVM_SETITEMSTATE, ^uintptr(0), lp) { return newError("SendMessage(LVM_SETITEMSTATE)") } lvi.StateMask = win.LVIS_SELECTED lvi.State = win.LVIS_SELECTED j := -1 for i := 0; i < count; i++ { j = int(win.SendMessage(hwndFrom, win.LVM_GETNEXTITEM, uintptr(j), win.LVNI_SELECTED)) if win.FALSE == win.SendMessage(hwndTo, win.LVM_SETITEMSTATE, uintptr(j), lp) { return newError("SendMessage(LVM_SETITEMSTATE)") } } return nil } // ItemStateChangedEventDelay returns the delay in milliseconds, between the // moment the state of an item in the *TableView changes and the moment the // associated event is published. // // By default there is no delay. func (tv *TableView) ItemStateChangedEventDelay() int { return tv.itemStateChangedEventDelay } // SetItemStateChangedEventDelay sets the delay in milliseconds, between the // moment the state of an item in the *TableView changes and the moment the // associated event is published. // // An example where this may be useful is a master-details scenario. If the // master TableView is configured to delay the event, you can avoid pointless // updates of the details TableView, if the user uses arrow keys to rapidly // navigate the master view. func (tv *TableView) SetItemStateChangedEventDelay(delay int) { tv.itemStateChangedEventDelay = delay } // SelectedIndexesChanged returns the event that is published when the list of // selected item indexes changed. func (tv *TableView) SelectedIndexesChanged() *Event { return tv.selectedIndexesChangedPublisher.Event() } func (tv *TableView) publishSelectedIndexesChanged() { if tv.itemStateChangedEventDelay > 0 { if 0 == win.SetTimer( tv.hWnd, tableViewSelectedIndexesChangedTimerId, uint32(tv.itemStateChangedEventDelay), 0) { lastError("SetTimer") } } else { tv.selectedIndexesChangedPublisher.Publish() } } // LastColumnStretched returns if the last column should take up all remaining // horizontal space of the *TableView. func (tv *TableView) LastColumnStretched() bool { return tv.lastColumnStretched } // SetLastColumnStretched sets if the last column should take up all remaining // horizontal space of the *TableView. // // The effect of setting this is persistent. func (tv *TableView) SetLastColumnStretched(value bool) error { if value { if err := tv.StretchLastColumn(); err != nil { return err } } tv.lastColumnStretched = value return nil } // StretchLastColumn makes the last column take up all remaining horizontal // space of the *TableView. // // The effect of this is not persistent. func (tv *TableView) StretchLastColumn() error { colCount := tv.visibleColumnCount() if colCount == 0 { return nil } var hwnd win.HWND frozenColCount := tv.visibleFrozenColumnCount() if colCount-frozenColCount == 0 { hwnd = tv.hwndFrozenLV colCount = frozenColCount } else { hwnd = tv.hwndNormalLV colCount -= frozenColCount } var lp uintptr if tv.scrollbarOrientation&Horizontal != 0 { lp = win.LVSCW_AUTOSIZE_USEHEADER } else { width := tv.ClientBoundsPixels().Width lastIndexInLV := -1 var lastIndexInLVWidth int for _, tvc := range tv.columns.items { var offset int if !tvc.Frozen() { offset = frozenColCount } colWidth := tv.IntFrom96DPI(tvc.Width()) width -= colWidth if index := int32(offset) + tvc.indexInListView(); int(index) > lastIndexInLV { lastIndexInLV = int(index) lastIndexInLVWidth = colWidth } } width += lastIndexInLVWidth if hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.WS_VSCROLL) { width -= int(win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, uint32(tv.DPI()))) } lp = uintptr(maxi(0, width)) } if lp > 0 { if 0 == win.SendMessage(hwnd, win.LVM_SETCOLUMNWIDTH, uintptr(colCount-1), lp) { return newError("LVM_SETCOLUMNWIDTH failed") } if dpi := tv.DPI(); dpi != tv.dpiOfPrevStretchLastColumn { tv.dpiOfPrevStretchLastColumn = dpi tv.Invalidate() } } return nil } // Persistent returns if the *TableView should persist its UI state, like column // widths. See *App.Settings for details. func (tv *TableView) Persistent() bool { return tv.persistent } // SetPersistent sets if the *TableView should persist its UI state, like column // widths. See *App.Settings for details. func (tv *TableView) SetPersistent(value bool) { tv.persistent = value } // IgnoreNowhere returns if the *TableView should ignore left mouse clicks in the // empty space. It forbids the user from unselecting the current index, or when // multi selection is enabled, disables click drag selection. func (tv *TableView) IgnoreNowhere() bool { return tv.ignoreNowhere } // IgnoreNowhere sets if the *TableView should ignore left mouse clicks in the // empty space. It forbids the user from unselecting the current index, or when // multi selection is enabled, disables click drag selection. func (tv *TableView) SetIgnoreNowhere(value bool) { tv.ignoreNowhere = value } type tableViewState struct { SortColumnName string SortOrder SortOrder ColumnDisplayOrder []string Columns []*tableViewColumnState } type tableViewColumnState struct { Name string Title string Width int Visible bool Frozen bool LastSeenDate string } // SaveState writes the UI state of the *TableView to the settings. func (tv *TableView) SaveState() error { if tv.columns.Len() == 0 { return nil } if tv.state == nil { tv.state = new(tableViewState) } tvs := tv.state tvs.SortColumnName = tv.columns.items[tv.sortedColumnIndex].name tvs.SortOrder = tv.sortOrder // tvs.Columns = make([]tableViewColumnState, tv.columns.Len()) for _, tvc := range tv.columns.items { var tvcs *tableViewColumnState for _, cur := range tvs.Columns { if cur.Name == tvc.name { tvcs = cur break } } // tvcs := &tvs.Columns[i] if tvcs == nil { tvs.Columns = append(tvs.Columns, new(tableViewColumnState)) tvcs = tvs.Columns[len(tvs.Columns)-1] } tvcs.Name = tvc.name tvcs.Title = tvc.titleOverride tvcs.Width = tvc.Width() tvcs.Visible = tvc.Visible() tvcs.Frozen = tvc.Frozen() tvcs.LastSeenDate = time.Now().Format("2006-01-02") } visibleCols := tv.visibleColumns() frozenCount := tv.visibleFrozenColumnCount() normalCount := len(visibleCols) - frozenCount indices := make([]int32, len(visibleCols)) var lp uintptr if frozenCount > 0 { lp = uintptr(unsafe.Pointer(&indices[0])) if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(frozenCount), lp) { return newError("LVM_GETCOLUMNORDERARRAY") } } if normalCount > 0 { lp = uintptr(unsafe.Pointer(&indices[frozenCount])) if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(normalCount), lp) { return newError("LVM_GETCOLUMNORDERARRAY") } } tvs.ColumnDisplayOrder = make([]string, len(visibleCols)) for i, j := range indices { if i >= frozenCount { j += int32(frozenCount) } tvs.ColumnDisplayOrder[i] = visibleCols[j].name } state, err := json.Marshal(tvs) if err != nil { return err } return tv.WriteState(string(state)) } // RestoreState restores the UI state of the *TableView from the settings. func (tv *TableView) RestoreState() error { state, err := tv.ReadState() if err != nil { return err } if state == "" { return nil } tv.SetSuspended(true) defer tv.SetSuspended(false) if tv.state == nil { tv.state = new(tableViewState) } tvs := tv.state if err := json.Unmarshal(([]byte)(state), tvs); err != nil { return err } name2tvc := make(map[string]*TableViewColumn) for _, tvc := range tv.columns.items { name2tvc[tvc.name] = tvc } name2tvcs := make(map[string]*tableViewColumnState) tvcsRetained := make([]*tableViewColumnState, 0, len(tvs.Columns)) for _, tvcs := range tvs.Columns { if tvcs.LastSeenDate != "" { if lastSeen, err := time.Parse("2006-02-01", tvcs.LastSeenDate); err != nil { tvcs.LastSeenDate = "" } else if name2tvc[tvcs.Name] == nil && lastSeen.Add(time.Hour*24*90).Before(time.Now()) { continue } } tvcsRetained = append(tvcsRetained, tvcs) name2tvcs[tvcs.Name] = tvcsRetained[len(tvcsRetained)-1] if tvc := name2tvc[tvcs.Name]; tvc != nil { if err := tvc.SetTitleOverride(tvcs.Title); err != nil { return err } if err := tvc.SetWidth(tvcs.Width); err != nil { return err } var visible bool for _, name := range tvs.ColumnDisplayOrder { if name == tvc.name { visible = true break } } if err := tvc.SetVisible(tvc.visible && (visible || tvcs.Visible)); err != nil { return err } if err := tvc.SetFrozen(tvcs.Frozen); err != nil { return err } } } tvs.Columns = tvcsRetained visibleCount := tv.visibleColumnCount() frozenCount := tv.visibleFrozenColumnCount() normalCount := visibleCount - frozenCount indices := make([]int32, visibleCount) knownNames := make(map[string]struct{}) displayOrder := make([]string, 0, visibleCount) for _, name := range tvs.ColumnDisplayOrder { knownNames[name] = struct{}{} if tvc, ok := name2tvc[name]; ok && tvc.visible { displayOrder = append(displayOrder, name) } } for _, tvc := range tv.visibleColumns() { if _, ok := knownNames[tvc.name]; !ok { displayOrder = append(displayOrder, tvc.name) } } for i, tvc := range tv.visibleColumns() { for j, name := range displayOrder { if tvc.name == name && j < visibleCount { idx := i if j >= frozenCount { idx -= frozenCount } indices[j] = int32(idx) break } } } var lp uintptr if frozenCount > 0 { lp = uintptr(unsafe.Pointer(&indices[0])) if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETCOLUMNORDERARRAY, uintptr(frozenCount), lp) { return newError("LVM_SETCOLUMNORDERARRAY") } } if normalCount > 0 { lp = uintptr(unsafe.Pointer(&indices[frozenCount])) if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_SETCOLUMNORDERARRAY, uintptr(normalCount), lp) { return newError("LVM_SETCOLUMNORDERARRAY") } } for i, c := range tvs.Columns { if c.Name == tvs.SortColumnName && i < visibleCount { tv.sortedColumnIndex = i tv.sortOrder = tvs.SortOrder break } } if sorter, ok := tv.model.(Sorter); ok { if !sorter.ColumnSortable(tv.sortedColumnIndex) { for i := range tvs.Columns { if sorter.ColumnSortable(i) { tv.sortedColumnIndex = i break } } } sorter.Sort(tv.sortedColumnIndex, tvs.SortOrder) } return nil } func (tv *TableView) toggleItemChecked(index int) error { checked := tv.itemChecker.Checked(index) if err := tv.itemChecker.SetChecked(index, !checked); err != nil { return wrapError(err) } if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_UPDATE, uintptr(index), 0) { return newError("SendMessage(LVM_UPDATE)") } if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_UPDATE, uintptr(index), 0) { return newError("SendMessage(LVM_UPDATE)") } return nil } func (tv *TableView) applyImageListForImage(image interface{}) { tv.hIml, tv.usingSysIml, _ = imageListForImage(image, tv.DPI()) tv.applyImageList() tv.imageUintptr2Index = make(map[uintptr]int32) tv.filePath2IconIndex = make(map[string]int32) } func (tv *TableView) applyImageList() { win.SendMessage(tv.hwndFrozenLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, uintptr(tv.hIml)) win.SendMessage(tv.hwndNormalLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, uintptr(tv.hIml)) } func (tv *TableView) disposeImageListAndCaches() { if tv.hIml != 0 && !tv.usingSysIml { win.SendMessage(tv.hwndFrozenLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, 0) win.SendMessage(tv.hwndNormalLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, 0) win.ImageList_Destroy(tv.hIml) } tv.hIml = 0 tv.imageUintptr2Index = nil tv.filePath2IconIndex = nil } func (tv *TableView) Focused() bool { focused := win.GetFocus() return focused == tv.hwndFrozenLV || focused == tv.hwndNormalLV } func (tv *TableView) maybePublishFocusChanged(hwnd win.HWND, msg uint32, wp uintptr) { focused := msg == win.WM_SETFOCUS if focused != tv.focused && wp != uintptr(tv.hwndFrozenLV) && wp != uintptr(tv.hwndNormalLV) { tv.focused = focused tv.focusedChangedPublisher.Publish() } } func tableViewFrozenLVWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr { tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(hwnd)).AsWindowBase())) switch msg { case win.WM_NCCALCSIZE: ensureWindowLongBits(hwnd, win.GWL_STYLE, win.WS_HSCROLL|win.WS_VSCROLL, false) case win.WM_SETFOCUS: win.SetFocus(tv.hwndNormalLV) tv.maybePublishFocusChanged(hwnd, msg, wp) case win.WM_KILLFOCUS: tv.maybePublishFocusChanged(hwnd, msg, wp) case win.WM_MOUSEWHEEL: tableViewNormalLVWndProc(tv.hwndNormalLV, msg, wp, lp) } return tv.lvWndProc(tv.frozenLVOrigWndProcPtr, hwnd, msg, wp, lp) } func tableViewNormalLVWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr { tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(hwnd)).AsWindowBase())) switch msg { case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN: win.SetFocus(tv.hwndFrozenLV) case win.WM_SETFOCUS: tv.invalidateBorderInParent() tv.maybePublishFocusChanged(hwnd, msg, wp) case win.WM_KILLFOCUS: win.SendMessage(tv.hwndFrozenLV, msg, wp, lp) tv.WndProc(tv.hWnd, msg, wp, lp) tv.maybePublishFocusChanged(hwnd, msg, wp) } result := tv.lvWndProc(tv.normalLVOrigWndProcPtr, hwnd, msg, wp, lp) var off uint32 = win.WS_HSCROLL | win.WS_VSCROLL if tv.scrollbarOrientation&Horizontal != 0 { off &^= win.WS_HSCROLL } if tv.scrollbarOrientation&Vertical != 0 { off &^= win.WS_VSCROLL } if off != 0 { ensureWindowLongBits(hwnd, win.GWL_STYLE, off, false) } return result } func (tv *TableView) lvWndProc(origWndProcPtr uintptr, hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr { var hwndOther win.HWND if hwnd == tv.hwndFrozenLV { hwndOther = tv.hwndNormalLV } else { hwndOther = tv.hwndFrozenLV } var maybeStretchLastColumn bool switch msg { case win.WM_ERASEBKGND: maybeStretchLastColumn = true case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lp)) if wp.Flags&win.SWP_NOSIZE != 0 { break } maybeStretchLastColumn = int(wp.Cx) < tv.WidthPixels() case win.WM_GETDLGCODE: if wp == win.VK_RETURN { return win.DLGC_WANTALLKEYS } case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN, win.WM_LBUTTONDBLCLK, win.WM_RBUTTONDBLCLK: var hti win.LVHITTESTINFO hti.Pt = win.POINT{win.GET_X_LPARAM(lp), win.GET_Y_LPARAM(lp)} win.SendMessage(hwnd, win.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) tv.itemIndexOfLastMouseButtonDown = int(hti.IItem) if hti.Flags == win.LVHT_NOWHERE { if tv.MultiSelection() { tv.publishNextSelClear = true } else { if tv.CheckBoxes() { if tv.currentIndex > -1 { tv.SetCurrentIndex(-1) } } else { // We keep the current item, if in single item selection mode without check boxes. win.SetFocus(tv.hwndFrozenLV) return 0 } } if tv.IgnoreNowhere() { return 0 } } switch msg { case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN: if hti.Flags == win.LVHT_ONITEMSTATEICON && tv.itemChecker != nil && tv.CheckBoxes() { tv.toggleItemChecked(int(hti.IItem)) } case win.WM_LBUTTONDBLCLK, win.WM_RBUTTONDBLCLK: if tv.currentIndex != tv.prevIndex && tv.itemStateChangedEventDelay > 0 { tv.prevIndex = tv.currentIndex tv.currentIndexChangedPublisher.Publish() tv.currentItemChangedPublisher.Publish() } } case win.WM_LBUTTONUP, win.WM_RBUTTONUP: tv.itemIndexOfLastMouseButtonDown = -1 case win.WM_MOUSEMOVE, win.WM_MOUSELEAVE: if tv.inMouseEvent { break } tv.inMouseEvent = true defer func() { tv.inMouseEvent = false }() if msg == win.WM_MOUSEMOVE { y := int(win.GET_Y_LPARAM(lp)) lp = uintptr(win.MAKELONG(0, uint16(y))) } win.SendMessage(hwndOther, msg, wp, lp) case win.WM_KEYDOWN: if wp == win.VK_SPACE && tv.currentIndex > -1 && tv.itemChecker != nil && tv.CheckBoxes() { tv.toggleItemChecked(tv.currentIndex) } tv.handleKeyDown(wp, lp) case win.WM_KEYUP: tv.handleKeyUp(wp, lp) case win.WM_NOTIFY: nmh := ((*win.NMHDR)(unsafe.Pointer(lp))) switch nmh.HwndFrom { case tv.hwndFrozenHdr, tv.hwndNormalHdr: if nmh.Code == win.NM_CUSTOMDRAW { return tableViewHdrWndProc(nmh.HwndFrom, msg, wp, lp) } } switch nmh.Code { case win.LVN_GETDISPINFO: di := (*win.NMLVDISPINFO)(unsafe.Pointer(lp)) row := int(di.Item.IItem) col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, di.Item.ISubItem) if col == -1 { break } if di.Item.Mask&win.LVIF_TEXT > 0 { value := tv.model.Value(row, col) var text string if format := tv.columns.items[col].formatFunc; format != nil { text = format(value) } else { switch val := value.(type) { case string: text = val case float32: prec := tv.columns.items[col].precision if prec == 0 { prec = 2 } text = FormatFloatGrouped(float64(val), prec) case float64: prec := tv.columns.items[col].precision if prec == 0 { prec = 2 } text = FormatFloatGrouped(val, prec) case time.Time: if val.Year() > 1601 { text = val.Format(tv.columns.items[col].format) } case bool: if val { text = checkmark } case *big.Rat: prec := tv.columns.items[col].precision if prec == 0 { prec = 2 } text = formatBigRatGrouped(val, prec) default: text = fmt.Sprintf(tv.columns.items[col].format, val) } } utf16 := syscall.StringToUTF16(text) buf := (*[264]uint16)(unsafe.Pointer(di.Item.PszText)) max := mini(len(utf16), int(di.Item.CchTextMax)) copy((*buf)[:], utf16[:max]) (*buf)[max-1] = 0 } if (tv.imageProvider != nil || tv.styler != nil) && di.Item.Mask&win.LVIF_IMAGE > 0 { var image interface{} if di.Item.ISubItem == 0 { if ip := tv.imageProvider; ip != nil && image == nil { image = ip.Image(row) } } if styler := tv.styler; styler != nil && image == nil { tv.style.row = row tv.style.col = col tv.style.bounds = Rectangle{} tv.style.dpi = tv.DPI() tv.style.Image = nil styler.StyleCell(&tv.style) image = tv.style.Image } if image != nil { if tv.hIml == 0 { tv.applyImageListForImage(image) } di.Item.IImage = imageIndexMaybeAdd( image, tv.hIml, tv.usingSysIml, tv.imageUintptr2Index, tv.filePath2IconIndex, tv.DPI()) } } if di.Item.ISubItem == 0 && di.Item.StateMask&win.LVIS_STATEIMAGEMASK > 0 && tv.itemChecker != nil { checked := tv.itemChecker.Checked(row) if checked { di.Item.State = 0x2000 } else { di.Item.State = 0x1000 } } case win.NM_CUSTOMDRAW: nmlvcd := (*win.NMLVCUSTOMDRAW)(unsafe.Pointer(lp)) if nmlvcd.IIconPhase == 0 { row := int(nmlvcd.Nmcd.DwItemSpec) col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, nmlvcd.ISubItem) if col == -1 { break } applyCellStyle := func() int { if tv.styler != nil { dpi := tv.DPI() tv.style.row = row tv.style.col = col tv.style.bounds = rectangleFromRECT(nmlvcd.Nmcd.Rc) tv.style.dpi = dpi tv.style.hdc = nmlvcd.Nmcd.Hdc tv.style.BackgroundColor = tv.itemBGColor tv.style.TextColor = tv.itemTextColor tv.style.Font = nil tv.style.Image = nil tv.styler.StyleCell(&tv.style) defer func() { tv.style.bounds = Rectangle{} if tv.style.canvas != nil { tv.style.canvas.Dispose() tv.style.canvas = nil } tv.style.hdc = 0 }() if tv.style.canvas != nil { return win.CDRF_SKIPDEFAULT } nmlvcd.ClrTextBk = win.COLORREF(tv.style.BackgroundColor) nmlvcd.ClrText = win.COLORREF(tv.style.TextColor) if font := tv.style.Font; font != nil { win.SelectObject(nmlvcd.Nmcd.Hdc, win.HGDIOBJ(font.handleForDPI(dpi))) } } return 0 } switch nmlvcd.Nmcd.DwDrawStage { case win.CDDS_PREPAINT: return win.CDRF_NOTIFYITEMDRAW case win.CDDS_ITEMPREPAINT: var selected bool if itemState := win.SendMessage(hwnd, win.LVM_GETITEMSTATE, nmlvcd.Nmcd.DwItemSpec, win.LVIS_SELECTED); itemState&win.LVIS_SELECTED != 0 { selected = true tv.itemBGColor = tv.themeSelectedBGColor tv.itemTextColor = tv.themeSelectedTextColor } else { tv.itemBGColor = tv.themeNormalBGColor tv.itemTextColor = tv.themeNormalTextColor } if !selected && tv.alternatingRowBG && row%2 == 1 { tv.itemBGColor = tv.alternatingRowBGColor tv.itemTextColor = tv.alternatingRowTextColor } tv.style.BackgroundColor = tv.itemBGColor tv.style.TextColor = tv.itemTextColor if tv.styler != nil { tv.style.row = row tv.style.col = -1 tv.style.bounds = rectangleFromRECT(nmlvcd.Nmcd.Rc) tv.style.dpi = tv.DPI() tv.style.hdc = 0 tv.style.Font = nil tv.style.Image = nil tv.styler.StyleCell(&tv.style) tv.itemFont = tv.style.Font } if selected { tv.style.BackgroundColor = tv.itemBGColor tv.style.TextColor = tv.itemTextColor } else { tv.itemBGColor = tv.style.BackgroundColor tv.itemTextColor = tv.style.TextColor } if tv.style.BackgroundColor != tv.themeNormalBGColor { var color Color if selected && !tv.Focused() { color = tv.themeSelectedNotFocusedBGColor } else { color = tv.style.BackgroundColor } if brush, _ := NewSolidColorBrush(color); brush != nil { defer brush.Dispose() canvas, _ := newCanvasFromHDC(nmlvcd.Nmcd.Hdc) canvas.FillRectanglePixels(brush, rectangleFromRECT(nmlvcd.Nmcd.Rc)) } } nmlvcd.ClrText = win.COLORREF(tv.style.TextColor) nmlvcd.ClrTextBk = win.COLORREF(tv.style.BackgroundColor) return win.CDRF_NOTIFYSUBITEMDRAW case win.CDDS_ITEMPREPAINT | win.CDDS_SUBITEM: if tv.itemFont != nil { win.SelectObject(nmlvcd.Nmcd.Hdc, win.HGDIOBJ(tv.itemFont.handleForDPI(tv.DPI()))) } if applyCellStyle() == win.CDRF_SKIPDEFAULT && win.IsAppThemed() { return win.CDRF_SKIPDEFAULT } return win.CDRF_NEWFONT | win.CDRF_SKIPPOSTPAINT | win.CDRF_NOTIFYPOSTPAINT case win.CDDS_ITEMPOSTPAINT | win.CDDS_SUBITEM: if applyCellStyle() == win.CDRF_SKIPDEFAULT { return win.CDRF_SKIPDEFAULT } return win.CDRF_NEWFONT | win.CDRF_SKIPPOSTPAINT } return win.CDRF_SKIPPOSTPAINT } return win.CDRF_SKIPPOSTPAINT case win.LVN_BEGINSCROLL: if tv.scrolling { break } tv.scrolling = true defer func() { tv.scrolling = false }() var rc win.RECT win.SendMessage(hwnd, win.LVM_GETITEMRECT, 0, uintptr(unsafe.Pointer(&rc))) nmlvs := (*win.NMLVSCROLL)(unsafe.Pointer(lp)) win.SendMessage(hwndOther, win.LVM_SCROLL, 0, uintptr(nmlvs.Dy*(rc.Bottom-rc.Top))) case win.LVN_COLUMNCLICK: nmlv := (*win.NMLISTVIEW)(unsafe.Pointer(lp)) col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, nmlv.ISubItem) if sorter, ok := tv.model.(Sorter); ok && sorter.ColumnSortable(col) { prevCol := sorter.SortedColumn() var order SortOrder if col != prevCol || sorter.SortOrder() == SortDescending { order = SortAscending } else { order = SortDescending } tv.sortedColumnIndex = col tv.sortOrder = order sorter.Sort(col, order) } tv.columnClickedPublisher.Publish(col) case win.LVN_ITEMCHANGED: nmlv := (*win.NMLISTVIEW)(unsafe.Pointer(lp)) if tv.hwndItemChanged != 0 && tv.hwndItemChanged != hwnd { break } tv.hwndItemChanged = hwnd defer func() { tv.hwndItemChanged = 0 }() tv.copySelectedIndexes(hwndOther, hwnd) if nmlv.IItem == -1 && !tv.publishNextSelClear { break } tv.publishNextSelClear = false selectedNow := nmlv.UNewState&win.LVIS_SELECTED > 0 selectedBefore := nmlv.UOldState&win.LVIS_SELECTED > 0 if tv.itemIndexOfLastMouseButtonDown != -1 && selectedNow && !selectedBefore && ModifiersDown()&(ModControl|ModShift) == 0 { tv.prevIndex = tv.currentIndex tv.currentIndex = int(nmlv.IItem) if tv.itemStateChangedEventDelay > 0 { tv.delayedCurrentIndexChangedCanceled = false if 0 == win.SetTimer( tv.hWnd, tableViewCurrentIndexChangedTimerId, uint32(tv.itemStateChangedEventDelay), 0) { lastError("SetTimer") } tv.SetCurrentIndex(int(nmlv.IItem)) } else { tv.SetCurrentIndex(int(nmlv.IItem)) } } if selectedNow != selectedBefore { if !tv.inSetSelectedIndexes && tv.MultiSelection() { tv.updateSelectedIndexes() } } case win.LVN_ODSTATECHANGED: if tv.hwndItemChanged != 0 && tv.hwndItemChanged != hwnd { break } tv.hwndItemChanged = hwnd defer func() { tv.hwndItemChanged = 0 }() tv.copySelectedIndexes(hwndOther, hwnd) tv.updateSelectedIndexes() case win.LVN_ITEMACTIVATE: nmia := (*win.NMITEMACTIVATE)(unsafe.Pointer(lp)) if tv.itemStateChangedEventDelay > 0 { tv.delayedCurrentIndexChangedCanceled = true } if int(nmia.IItem) != tv.currentIndex { tv.SetCurrentIndex(int(nmia.IItem)) tv.currentIndexChangedPublisher.Publish() tv.currentItemChangedPublisher.Publish() } tv.itemActivatedPublisher.Publish() case win.HDN_ITEMCHANGING: tv.updateLVSizes() } case win.WM_UPDATEUISTATE: switch win.LOWORD(uint32(wp)) { case win.UIS_SET: wp |= win.UISF_HIDEFOCUS << 16 case win.UIS_CLEAR, win.UIS_INITIALIZE: wp &^= ^uintptr(win.UISF_HIDEFOCUS << 16) } } lpFixed := lp fixXInLP := func() { // fmt.Printf("hwnd == tv.hwndNormalLV: %t, tv.hasFrozenColumn: %t\n", hwnd == tv.hwndNormalLV, tv.hasFrozenColumn) if hwnd == tv.hwndNormalLV && tv.hasFrozenColumn { var rc win.RECT if win.GetWindowRect(tv.hwndFrozenLV, &rc) { x := int(win.GET_X_LPARAM(lp)) + int(rc.Right-rc.Left) y := int(win.GET_Y_LPARAM(lp)) lpFixed = uintptr(win.MAKELONG(uint16(x), uint16(y))) } } } switch msg { case win.WM_LBUTTONDOWN, win.WM_MBUTTONDOWN, win.WM_RBUTTONDOWN: fixXInLP() tv.publishMouseEvent(&tv.mouseDownPublisher, msg, wp, lpFixed) case win.WM_LBUTTONUP, win.WM_MBUTTONUP, win.WM_RBUTTONUP: fixXInLP() tv.publishMouseEvent(&tv.mouseUpPublisher, msg, wp, lpFixed) case win.WM_MOUSEMOVE: fixXInLP() tv.publishMouseEvent(&tv.mouseMovePublisher, msg, wp, lpFixed) case win.WM_MOUSEWHEEL: fixXInLP() tv.publishMouseWheelEvent(&tv.mouseWheelPublisher, wp, lpFixed) } if maybeStretchLastColumn { if tv.lastColumnStretched && !tv.busyStretchingLastColumn { if normalVisColCount := tv.visibleColumnCount() - tv.visibleFrozenColumnCount(); normalVisColCount == 0 || normalVisColCount > 0 == (hwnd == tv.hwndNormalLV) { tv.busyStretchingLastColumn = true defer func() { tv.busyStretchingLastColumn = false }() tv.StretchLastColumn() } } if msg == win.WM_ERASEBKGND { return 1 } } return win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp) } func tableViewHdrWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr { tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(win.GetParent(hwnd))).AsWindowBase())) var origWndProcPtr uintptr if hwnd == tv.hwndFrozenHdr { origWndProcPtr = tv.frozenHdrOrigWndProcPtr } else { origWndProcPtr = tv.normalHdrOrigWndProcPtr } switch msg { case win.WM_NOTIFY: switch ((*win.NMHDR)(unsafe.Pointer(lp))).Code { case win.NM_CUSTOMDRAW: if tv.customHeaderHeight == 0 { break } nmcd := (*win.NMCUSTOMDRAW)(unsafe.Pointer(lp)) switch nmcd.DwDrawStage { case win.CDDS_PREPAINT: return win.CDRF_NOTIFYITEMDRAW case win.CDDS_ITEMPREPAINT: return win.CDRF_NOTIFYPOSTPAINT case win.CDDS_ITEMPOSTPAINT: col := tv.fromLVColIdx(hwnd == tv.hwndFrozenHdr, int32(nmcd.DwItemSpec)) if tv.styler != nil && col > -1 { tv.style.row = -1 tv.style.col = col tv.style.bounds = rectangleFromRECT(nmcd.Rc) tv.style.dpi = tv.DPI() tv.style.hdc = nmcd.Hdc tv.style.TextColor = tv.themeNormalTextColor tv.style.Font = nil tv.styler.StyleCell(&tv.style) defer func() { tv.style.bounds = Rectangle{} if tv.style.canvas != nil { tv.style.canvas.Dispose() tv.style.canvas = nil } tv.style.hdc = 0 }() } return win.CDRF_DODEFAULT } return win.CDRF_DODEFAULT } case win.HDM_LAYOUT: if tv.customHeaderHeight == 0 { break } result := win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp) hdl := (*win.HDLAYOUT)(unsafe.Pointer(lp)) hdl.Prc.Top = int32(tv.customHeaderHeight) hdl.Pwpos.Cy = int32(tv.customHeaderHeight) return result case win.WM_MOUSEMOVE, win.WM_LBUTTONDOWN, win.WM_LBUTTONUP, win.WM_MBUTTONDOWN, win.WM_MBUTTONUP, win.WM_RBUTTONDOWN, win.WM_RBUTTONUP: hti := win.HDHITTESTINFO{Pt: win.POINT{int32(win.GET_X_LPARAM(lp)), int32(win.GET_Y_LPARAM(lp))}} win.SendMessage(hwnd, win.HDM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) if hti.IItem == -1 { tv.group.toolTip.setText(hwnd, "") break } col := tv.fromLVColIdx(hwnd == tv.hwndFrozenHdr, hti.IItem) text := tv.columns.At(col).TitleEffective() var rc win.RECT if 0 == win.SendMessage(hwnd, win.HDM_GETITEMRECT, uintptr(hti.IItem), uintptr(unsafe.Pointer(&rc))) { tv.group.toolTip.setText(hwnd, "") break } size := calculateTextSize(text, tv.Font(), tv.DPI(), 0, hwnd) if size.Width <= rectangleFromRECT(rc).Width-int(win.SendMessage(hwnd, win.HDM_GETBITMAPMARGIN, 0, 0)) { tv.group.toolTip.setText(hwnd, "") break } if tv.group.toolTip.text(hwnd) == text { break } tv.group.toolTip.setText(hwnd, text) m := win.MSG{ HWnd: hwnd, Message: msg, WParam: wp, LParam: lp, Pt: hti.Pt, } tv.group.toolTip.SendMessage(win.TTM_RELAYEVENT, 0, uintptr(unsafe.Pointer(&m))) } return win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp) } func (tv *TableView) WndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr { switch msg { case win.WM_NOTIFY: nmh := (*win.NMHDR)(unsafe.Pointer(lp)) switch nmh.HwndFrom { case tv.hwndFrozenLV: return tableViewFrozenLVWndProc(nmh.HwndFrom, msg, wp, lp) case tv.hwndNormalLV: return tableViewNormalLVWndProc(nmh.HwndFrom, msg, wp, lp) } case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lp)) if wp.Flags&win.SWP_NOSIZE != 0 { break } if tv.formActivatingHandle == -1 { if form := tv.Form(); form != nil { tv.formActivatingHandle = form.Activating().Attach(func() { if tv.hwndNormalLV == win.GetFocus() { win.SetFocus(tv.hwndFrozenLV) } }) } } tv.updateLVSizes() // FIXME: The InvalidateRect and redrawItems calls below prevent // painting glitches on resize. Though this seems to work reasonably // well, in the long run we would like to find the root cause of this // issue and come up with a better fix. dpi := uint32(tv.DPI()) var rc win.RECT vsbWidth := win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, dpi) rc = win.RECT{wp.Cx - vsbWidth - 1, 0, wp.Cx, wp.Cy} win.InvalidateRect(tv.hWnd, &rc, true) hsbHeight := win.GetSystemMetricsForDpi(win.SM_CYHSCROLL, dpi) rc = win.RECT{0, wp.Cy - hsbHeight - 1, wp.Cx, wp.Cy} win.InvalidateRect(tv.hWnd, &rc, true) tv.redrawItems() case win.WM_TIMER: if !win.KillTimer(tv.hWnd, wp) { lastError("KillTimer") } switch wp { case tableViewCurrentIndexChangedTimerId: if !tv.delayedCurrentIndexChangedCanceled { tv.currentIndexChangedPublisher.Publish() tv.currentItemChangedPublisher.Publish() } case tableViewSelectedIndexesChangedTimerId: tv.selectedIndexesChangedPublisher.Publish() } case win.WM_MEASUREITEM: mis := (*win.MEASUREITEMSTRUCT)(unsafe.Pointer(lp)) mis.ItemHeight = uint32(tv.customRowHeight) ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_OWNERDRAWFIXED, false) ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_OWNERDRAWFIXED, false) case win.WM_SETFOCUS: win.SetFocus(tv.hwndFrozenLV) case win.WM_DESTROY: // As we subclass all windows of system classes, we prevented the // clean-up code in the WM_NCDESTROY handlers of some windows from // being called. To fix this, we restore the original window // procedures here. if tv.frozenHdrOrigWndProcPtr != 0 { win.SetWindowLongPtr(tv.hwndFrozenHdr, win.GWLP_WNDPROC, tv.frozenHdrOrigWndProcPtr) } if tv.frozenLVOrigWndProcPtr != 0 { win.SetWindowLongPtr(tv.hwndFrozenLV, win.GWLP_WNDPROC, tv.frozenLVOrigWndProcPtr) } if tv.normalHdrOrigWndProcPtr != 0 { win.SetWindowLongPtr(tv.hwndNormalHdr, win.GWLP_WNDPROC, tv.normalHdrOrigWndProcPtr) } if tv.normalLVOrigWndProcPtr != 0 { win.SetWindowLongPtr(tv.hwndNormalLV, win.GWLP_WNDPROC, tv.normalLVOrigWndProcPtr) } } return tv.WidgetBase.WndProc(hwnd, msg, wp, lp) } func (tv *TableView) updateLVSizes() { tv.updateLVSizesWithSpecialCare(false) } func (tv *TableView) updateLVSizesWithSpecialCare(needSpecialCare bool) { var width int for i := tv.columns.Len() - 1; i >= 0; i-- { if col := tv.columns.At(i); col.frozen { width += col.Width() } } dpi := tv.DPI() widthPixels := IntFrom96DPI(width, dpi) cb := tv.ClientBoundsPixels() win.MoveWindow(tv.hwndNormalLV, int32(widthPixels), 0, int32(cb.Width-widthPixels), int32(cb.Height), true) var sbh int if hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.WS_HSCROLL) { sbh = int(win.GetSystemMetricsForDpi(win.SM_CYHSCROLL, uint32(dpi))) } win.MoveWindow(tv.hwndFrozenLV, 0, 0, int32(widthPixels), int32(cb.Height-sbh), true) if needSpecialCare { tv.updateLVSizesNeedsSpecialCare = true } if tv.updateLVSizesNeedsSpecialCare { win.ShowWindow(tv.hwndNormalLV, win.SW_HIDE) win.ShowWindow(tv.hwndNormalLV, win.SW_SHOW) } if !needSpecialCare { tv.updateLVSizesNeedsSpecialCare = false } } func (*TableView) CreateLayoutItem(ctx *LayoutContext) LayoutItem { return NewGreedyLayoutItem() } func (tv *TableView) SetScrollbarOrientation(orientation Orientation) { tv.scrollbarOrientation = orientation } func (tv *TableView) ScrollbarOrientation() Orientation { return tv.scrollbarOrientation }