// Copyright 2010 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 ( "syscall" "unsafe" "github.com/lxn/win" ) type treeViewItemInfo struct { handle win.HTREEITEM child2Handle map[TreeItem]win.HTREEITEM } type TreeView struct { WidgetBase model TreeModel lazyPopulation bool itemsResetEventHandlerHandle int itemChangedEventHandlerHandle int itemInsertedEventHandlerHandle int itemRemovedEventHandlerHandle int item2Info map[TreeItem]*treeViewItemInfo handle2Item map[win.HTREEITEM]TreeItem currItem TreeItem hIml win.HIMAGELIST usingSysIml bool imageUintptr2Index map[uintptr]int32 filePath2IconIndex map[string]int32 expandedChangedPublisher TreeItemEventPublisher currentItemChangedPublisher EventPublisher itemActivatedPublisher EventPublisher } func NewTreeView(parent Container) (*TreeView, error) { tv := new(TreeView) if err := InitWidget( tv, parent, "SysTreeView32", win.WS_TABSTOP|win.WS_VISIBLE|win.TVS_HASBUTTONS|win.TVS_LINESATROOT|win.TVS_SHOWSELALWAYS|win.TVS_TRACKSELECT, win.WS_EX_CLIENTEDGE); err != nil { return nil, err } succeeded := false defer func() { if !succeeded { tv.Dispose() } }() if hr := win.HRESULT(tv.SendMessage(win.TVM_SETEXTENDEDSTYLE, win.TVS_EX_DOUBLEBUFFER, win.TVS_EX_DOUBLEBUFFER)); win.FAILED(hr) { return nil, errorFromHRESULT("TVM_SETEXTENDEDSTYLE", hr) } if err := tv.setTheme("Explorer"); err != nil { return nil, err } tv.GraphicsEffects().Add(InteractionEffect) tv.GraphicsEffects().Add(FocusEffect) tv.MustRegisterProperty("CurrentItem", NewReadOnlyProperty( func() interface{} { return tv.CurrentItem() }, tv.CurrentItemChanged())) tv.MustRegisterProperty("CurrentItemLevel", NewReadOnlyProperty( func() interface{} { level := -1 item := tv.CurrentItem() for item != nil { level++ item = item.Parent() } return level }, tv.CurrentItemChanged())) tv.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty( func() bool { return tv.CurrentItem() != nil }, tv.CurrentItemChanged())) succeeded = true return tv, nil } func (tv *TreeView) Dispose() { tv.WidgetBase.Dispose() tv.disposeImageListAndCaches() } func (tv *TreeView) SetBackground(bg Brush) { tv.WidgetBase.SetBackground(bg) color := Color(win.GetSysColor(win.COLOR_WINDOW)) if bg != nil { type Colorer interface { Color() Color } if c, ok := bg.(Colorer); ok { color = c.Color() } } tv.SendMessage(win.TVM_SETBKCOLOR, 0, uintptr(color)) } func (tv *TreeView) Model() TreeModel { return tv.model } func (tv *TreeView) SetModel(model TreeModel) error { if tv.model != nil { tv.model.ItemsReset().Detach(tv.itemsResetEventHandlerHandle) tv.model.ItemChanged().Detach(tv.itemChangedEventHandlerHandle) tv.model.ItemInserted().Detach(tv.itemInsertedEventHandlerHandle) tv.model.ItemRemoved().Detach(tv.itemRemovedEventHandlerHandle) tv.disposeImageListAndCaches() } tv.model = model if model != nil { tv.lazyPopulation = model.LazyPopulation() tv.itemsResetEventHandlerHandle = model.ItemsReset().Attach(func(parent TreeItem) { if parent == nil { tv.resetItems() } else if tv.item2Info[parent] != nil { tv.SetSuspended(true) defer tv.SetSuspended(false) if err := tv.removeDescendants(parent); err != nil { return } if err := tv.insertChildren(parent); err != nil { return } } }) tv.itemChangedEventHandlerHandle = model.ItemChanged().Attach(func(item TreeItem) { if item == nil || tv.item2Info[item] == nil { return } if err := tv.updateItem(item); err != nil { return } }) tv.itemInsertedEventHandlerHandle = model.ItemInserted().Attach(func(item TreeItem) { tv.SetSuspended(true) defer tv.SetSuspended(false) var hInsertAfter win.HTREEITEM parent := item.Parent() for i := parent.ChildCount() - 1; i >= 0; i-- { if parent.ChildAt(i) == item { if i > 0 { hInsertAfter = tv.item2Info[parent.ChildAt(i-1)].handle } else { hInsertAfter = win.TVI_FIRST } } } if _, err := tv.insertItemAfter(item, hInsertAfter); err != nil { return } }) tv.itemRemovedEventHandlerHandle = model.ItemRemoved().Attach(func(item TreeItem) { if err := tv.removeItem(item); err != nil { return } }) } return tv.resetItems() } func (tv *TreeView) CurrentItem() TreeItem { return tv.currItem } func (tv *TreeView) SetCurrentItem(item TreeItem) error { if item == tv.currItem { return nil } if item != nil { if err := tv.ensureItemAndAncestorsInserted(item); err != nil { return err } } handle, err := tv.handleForItem(item) if err != nil { return err } if 0 == tv.SendMessage(win.TVM_SELECTITEM, win.TVGN_CARET, uintptr(handle)) { return newError("SendMessage(TVM_SELECTITEM) failed") } tv.currItem = item return nil } func (tv *TreeView) EnsureVisible(item TreeItem) error { handle, err := tv.handleForItem(item) if err != nil { return err } tv.SendMessage(win.TVM_ENSUREVISIBLE, 0, uintptr(handle)) return nil } func (tv *TreeView) handleForItem(item TreeItem) (win.HTREEITEM, error) { if item != nil { if info := tv.item2Info[item]; info == nil { return 0, newError("invalid item") } else { return info.handle, nil } } return 0, newError("invalid item") } // ItemAt determines the location of the specified point in native pixels relative to the client area of a tree-view control. func (tv *TreeView) ItemAt(x, y int) TreeItem { hti := win.TVHITTESTINFO{Pt: Point{x, y}.toPOINT()} tv.SendMessage(win.TVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti))) if item, ok := tv.handle2Item[hti.HItem]; ok { return item } return nil } // ItemHeight returns the height of each item in native pixels. func (tv *TreeView) ItemHeight() int { return int(tv.SendMessage(win.TVM_GETITEMHEIGHT, 0, 0)) } // SetItemHeight sets the height of the tree-view items in native pixels. func (tv *TreeView) SetItemHeight(height int) { tv.SendMessage(win.TVM_SETITEMHEIGHT, uintptr(height), 0) } func (tv *TreeView) resetItems() error { tv.SetSuspended(true) defer tv.SetSuspended(false) if err := tv.clearItems(); err != nil { return err } if tv.model == nil { return nil } if err := tv.insertRoots(); err != nil { return err } return nil } func (tv *TreeView) clearItems() error { if 0 == tv.SendMessage(win.TVM_DELETEITEM, 0, 0) { return newError("SendMessage(TVM_DELETEITEM) failed") } tv.item2Info = make(map[TreeItem]*treeViewItemInfo) tv.handle2Item = make(map[win.HTREEITEM]TreeItem) return nil } func (tv *TreeView) insertRoots() error { for i := tv.model.RootCount() - 1; i >= 0; i-- { if _, err := tv.insertItem(tv.model.RootAt(i)); err != nil { return err } } return nil } func (tv *TreeView) ApplyDPI(dpi int) { tv.WidgetBase.ApplyDPI(dpi) tv.disposeImageListAndCaches() } func (tv *TreeView) applyImageListForImage(image interface{}) { tv.hIml, tv.usingSysIml, _ = imageListForImage(image, tv.DPI()) tv.SendMessage(win.TVM_SETIMAGELIST, 0, uintptr(tv.hIml)) tv.imageUintptr2Index = make(map[uintptr]int32) tv.filePath2IconIndex = make(map[string]int32) } func (tv *TreeView) disposeImageListAndCaches() { if tv.hIml != 0 && !tv.usingSysIml { win.ImageList_Destroy(tv.hIml) } tv.hIml = 0 tv.imageUintptr2Index = nil tv.filePath2IconIndex = nil } func (tv *TreeView) setTVITEMImageInfo(tvi *win.TVITEM, item TreeItem) { if imager, ok := item.(Imager); ok { if tv.hIml == 0 { tv.applyImageListForImage(imager.Image()) } // FIXME: If not setting TVIF_SELECTEDIMAGE and tvi.ISelectedImage, // some default icon will show up, even though we have not asked for it. tvi.Mask |= win.TVIF_IMAGE | win.TVIF_SELECTEDIMAGE tvi.IImage = imageIndexMaybeAdd( imager.Image(), tv.hIml, tv.usingSysIml, tv.imageUintptr2Index, tv.filePath2IconIndex, tv.DPI()) tvi.ISelectedImage = tvi.IImage } } func (tv *TreeView) insertItem(item TreeItem) (win.HTREEITEM, error) { return tv.insertItemAfter(item, win.TVI_FIRST) } func (tv *TreeView) insertItemAfter(item TreeItem, hInsertAfter win.HTREEITEM) (win.HTREEITEM, error) { var tvins win.TVINSERTSTRUCT tvi := &tvins.Item tvi.Mask = win.TVIF_CHILDREN | win.TVIF_TEXT tvi.PszText = win.LPSTR_TEXTCALLBACK tvi.CChildren = win.I_CHILDRENCALLBACK tv.setTVITEMImageInfo(tvi, item) parent := item.Parent() if parent == nil { tvins.HParent = win.TVI_ROOT } else { info := tv.item2Info[parent] if info == nil { return 0, newError("invalid parent") } tvins.HParent = info.handle } tvins.HInsertAfter = hInsertAfter hItem := win.HTREEITEM(tv.SendMessage(win.TVM_INSERTITEM, 0, uintptr(unsafe.Pointer(&tvins)))) if hItem == 0 { return 0, newError("TVM_INSERTITEM failed") } tv.item2Info[item] = &treeViewItemInfo{hItem, make(map[TreeItem]win.HTREEITEM)} tv.handle2Item[hItem] = item if !tv.lazyPopulation { if err := tv.insertChildren(item); err != nil { return 0, err } } return hItem, nil } func (tv *TreeView) insertChildren(parent TreeItem) error { info := tv.item2Info[parent] for i := parent.ChildCount() - 1; i >= 0; i-- { child := parent.ChildAt(i) if handle, err := tv.insertItem(child); err != nil { return err } else { info.child2Handle[child] = handle } } return nil } func (tv *TreeView) updateItem(item TreeItem) error { tvi := &win.TVITEM{ Mask: win.TVIF_TEXT, HItem: tv.item2Info[item].handle, PszText: win.LPSTR_TEXTCALLBACK, } tv.setTVITEMImageInfo(tvi, item) if 0 == tv.SendMessage(win.TVM_SETITEM, 0, uintptr(unsafe.Pointer(tvi))) { return newError("SendMessage(TVM_SETITEM) failed") } return nil } func (tv *TreeView) removeItem(item TreeItem) error { if err := tv.removeDescendants(item); err != nil { return err } info := tv.item2Info[item] if info == nil { return newError("invalid item") } if 0 == tv.SendMessage(win.TVM_DELETEITEM, 0, uintptr(info.handle)) { return newError("SendMessage(TVM_DELETEITEM) failed") } if parentInfo := tv.item2Info[item.Parent()]; parentInfo != nil { delete(parentInfo.child2Handle, item) } delete(tv.item2Info, item) delete(tv.handle2Item, info.handle) return nil } func (tv *TreeView) removeDescendants(parent TreeItem) error { for item, _ := range tv.item2Info[parent].child2Handle { if err := tv.removeItem(item); err != nil { return err } } return nil } func (tv *TreeView) ensureItemAndAncestorsInserted(item TreeItem) error { if item == nil { return newError("invalid item") } tv.SetSuspended(true) defer tv.SetSuspended(false) var hierarchy []TreeItem for item != nil && tv.item2Info[item] == nil { item = item.Parent() if item != nil { hierarchy = append(hierarchy, item) } else { return newError("invalid item") } } for i := len(hierarchy) - 1; i >= 0; i-- { if err := tv.insertChildren(hierarchy[i]); err != nil { return err } } return nil } func (tv *TreeView) Expanded(item TreeItem) bool { if tv.item2Info[item] == nil { return false } tvi := &win.TVITEM{ HItem: tv.item2Info[item].handle, Mask: win.TVIF_STATE, StateMask: win.TVIS_EXPANDED, } if 0 == tv.SendMessage(win.TVM_GETITEM, 0, uintptr(unsafe.Pointer(tvi))) { newError("SendMessage(TVM_GETITEM) failed") } return tvi.State&win.TVIS_EXPANDED != 0 } func (tv *TreeView) SetExpanded(item TreeItem, expanded bool) error { if expanded { if err := tv.ensureItemAndAncestorsInserted(item); err != nil { return err } } info := tv.item2Info[item] if info == nil { return newError("invalid item") } var action uintptr if expanded { action = win.TVE_EXPAND } else { action = win.TVE_COLLAPSE } if 0 == tv.SendMessage(win.TVM_EXPAND, action, uintptr(info.handle)) { return newError("SendMessage(TVM_EXPAND) failed") } return nil } func (tv *TreeView) ExpandedChanged() *TreeItemEvent { return tv.expandedChangedPublisher.Event() } func (tv *TreeView) CurrentItemChanged() *Event { return tv.currentItemChangedPublisher.Event() } func (tv *TreeView) ItemActivated() *Event { return tv.itemActivatedPublisher.Event() } func (tv *TreeView) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_GETDLGCODE: if wParam == win.VK_RETURN { return win.DLGC_WANTALLKEYS } case win.WM_NOTIFY: nmhdr := (*win.NMHDR)(unsafe.Pointer(lParam)) switch nmhdr.Code { case win.TVN_GETDISPINFO: nmtvdi := (*win.NMTVDISPINFO)(unsafe.Pointer(lParam)) item := tv.handle2Item[nmtvdi.Item.HItem] if nmtvdi.Item.Mask&win.TVIF_TEXT != 0 { var text string rc := win.RECT{Left: int32(nmtvdi.Item.HItem)} if 0 != tv.SendMessage(win.TVM_GETITEMRECT, 0, uintptr(unsafe.Pointer(&rc))) { // Only retrieve text if the item is visible. Why isn't Windows doing this for us? text = item.Text() } utf16 := syscall.StringToUTF16(text) buf := (*[264]uint16)(unsafe.Pointer(nmtvdi.Item.PszText)) max := mini(len(utf16), int(nmtvdi.Item.CchTextMax)) copy((*buf)[:], utf16[:max]) (*buf)[max-1] = 0 } if nmtvdi.Item.Mask&win.TVIF_CHILDREN != 0 { if hc, ok := item.(HasChilder); ok { if hc.HasChild() { nmtvdi.Item.CChildren = 1 } else { nmtvdi.Item.CChildren = 0 } } else { nmtvdi.Item.CChildren = int32(item.ChildCount()) } } case win.TVN_ITEMEXPANDING: nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam)) item := tv.handle2Item[nmtv.ItemNew.HItem] if nmtv.Action == win.TVE_EXPAND && tv.lazyPopulation { info := tv.item2Info[item] if len(info.child2Handle) == 0 { tv.insertChildren(item) } } case win.TVN_ITEMEXPANDED: nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam)) item := tv.handle2Item[nmtv.ItemNew.HItem] switch nmtv.Action { case win.TVE_COLLAPSE: tv.expandedChangedPublisher.Publish(item) case win.TVE_COLLAPSERESET: case win.TVE_EXPAND: tv.expandedChangedPublisher.Publish(item) case win.TVE_EXPANDPARTIAL: case win.TVE_TOGGLE: } case win.NM_DBLCLK: tv.itemActivatedPublisher.Publish() case win.TVN_KEYDOWN: nmtvkd := (*win.NMTVKEYDOWN)(unsafe.Pointer(lParam)) if nmtvkd.WVKey == uint16(KeyReturn) { tv.itemActivatedPublisher.Publish() } case win.TVN_SELCHANGED: nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam)) tv.currItem = tv.handle2Item[nmtv.ItemNew.HItem] tv.currentItemChangedPublisher.Publish() } } return tv.WidgetBase.WndProc(hwnd, msg, wParam, lParam) } func (*TreeView) NeedsWmSize() bool { return true } func (tv *TreeView) CreateLayoutItem(ctx *LayoutContext) LayoutItem { return NewGreedyLayoutItem() }