398 lines
11 KiB
C++
398 lines
11 KiB
C++
#pragma once
|
|
|
|
// ================================================================================
|
|
// CTreeMultiSel
|
|
// Implementation of multi-selection in a tree view ctrl
|
|
// Instantiate with dialog ID of your treeview,
|
|
// plug into your dialog's message map.
|
|
// Doesn't work correctly with explorer-themed tree controls (glitches happen).
|
|
// ================================================================================
|
|
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
class CTreeMultiSel : public CMessageMap {
|
|
public:
|
|
typedef std::set<HTREEITEM> selection_t;
|
|
typedef std::vector<HTREEITEM> selectionOrdered_t;
|
|
|
|
CTreeMultiSel(unsigned ID) : m_ID(ID) {}
|
|
|
|
BEGIN_MSG_MAP_EX(CTreeMultiSel)
|
|
NOTIFY_HANDLER_EX(m_ID, TVN_ITEMEXPANDED, OnItemExpanded)
|
|
NOTIFY_HANDLER_EX(m_ID, NM_CLICK, OnClick)
|
|
NOTIFY_HANDLER_EX(m_ID, TVN_DELETEITEM, OnItemDeleted)
|
|
NOTIFY_HANDLER_EX(m_ID, TVN_SELCHANGING, OnSelChanging)
|
|
NOTIFY_HANDLER_EX(m_ID, TVN_SELCHANGED, OnSelChangedFilter)
|
|
NOTIFY_HANDLER_EX(m_ID, NM_SETFOCUS, OnFocus)
|
|
NOTIFY_HANDLER_EX(m_ID, NM_KILLFOCUS, OnFocus)
|
|
END_MSG_MAP()
|
|
|
|
const unsigned m_ID;
|
|
|
|
// Retrieves selected items - on order of appearance in the view
|
|
selectionOrdered_t GetSelectionOrdered(CTreeViewCtrl tree) const {
|
|
HTREEITEM first = tree.GetRootItem();
|
|
selectionOrdered_t ret; ret.reserve( m_selection.size() );
|
|
for(HTREEITEM walk = first; walk != NULL; walk = tree.GetNextVisibleItem(walk)) {
|
|
if (m_selection.count(walk) > 0) ret.push_back( walk );
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
//! Undefined order! Use only when order of selected items is not relevant.
|
|
selection_t GetSelection() const { return m_selection; }
|
|
selection_t const & GetSelectionRef() const { return m_selection; }
|
|
bool IsItemSelected(HTREEITEM item) const {return m_selection.count(item) > 0;}
|
|
size_t GetSelCount() const {return m_selection.size();}
|
|
//! Retrieves a single-selection item. Null if nothing or more than one item is selected.
|
|
HTREEITEM GetSingleSel() const {
|
|
if (m_selection.size() != 1) return NULL;
|
|
return *m_selection.begin();
|
|
}
|
|
|
|
void OnContextMenu_FixSelection(CTreeViewCtrl tree, CPoint pt) {
|
|
if (pt != CPoint(-1, -1)) {
|
|
WIN32_OP_D(tree.ScreenToClient(&pt));
|
|
UINT flags = 0;
|
|
const HTREEITEM item = tree.HitTest(pt, &flags);
|
|
if (item != NULL && (flags & TVHT_ONITEM) != 0) {
|
|
if (!IsItemSelected(item)) {
|
|
SelectSingleItem(tree, item);
|
|
}
|
|
CallSelectItem(tree, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnLButtonDown(CTreeViewCtrl tree, WPARAM wp, LPARAM lp) {
|
|
if (!IsKeyPressed(VK_CONTROL)) {
|
|
UINT flags = 0;
|
|
HTREEITEM item = tree.HitTest(CPoint(lp), &flags);
|
|
if (item != NULL && (flags & TVHT_ONITEM) != 0) {
|
|
if (!IsItemSelected(item)) tree.SelectItem(item);
|
|
}
|
|
}
|
|
}
|
|
static bool IsNavKey(UINT vk) {
|
|
switch(vk) {
|
|
case VK_UP:
|
|
case VK_DOWN:
|
|
case VK_RIGHT:
|
|
case VK_LEFT:
|
|
case VK_PRIOR:
|
|
case VK_NEXT:
|
|
case VK_HOME:
|
|
case VK_END:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
BOOL OnChar(CTreeViewCtrl tree, WPARAM code) {
|
|
switch(code) {
|
|
case ' ':
|
|
if (IsKeyPressed(VK_CONTROL) || !IsTypingInProgress()) {
|
|
HTREEITEM item = tree.GetSelectedItem();
|
|
if (item != NULL) SelectToggleItem(tree, item);
|
|
return TRUE;
|
|
}
|
|
break;
|
|
}
|
|
m_lastTypingTime = GetTickCount(); m_lastTypingTimeValid = true;
|
|
return FALSE;
|
|
}
|
|
BOOL OnKeyDown(CTreeViewCtrl tree, UINT vKey) {
|
|
if (IsNavKey(vKey)) m_lastTypingTimeValid = false;
|
|
switch(vKey) {
|
|
case VK_UP:
|
|
if (IsKeyPressed(VK_CONTROL)) {
|
|
HTREEITEM item = tree.GetSelectedItem();
|
|
if (item != NULL) {
|
|
HTREEITEM prev = tree.GetPrevVisibleItem(item);
|
|
if (prev != NULL) {
|
|
CallSelectItem(tree, prev);
|
|
if (IsKeyPressed(VK_SHIFT)) {
|
|
if (m_selStart == NULL) m_selStart = item;
|
|
SelectItemRange(tree, prev);
|
|
}
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case VK_DOWN:
|
|
if (IsKeyPressed(VK_CONTROL)) {
|
|
HTREEITEM item = tree.GetSelectedItem();
|
|
if (item != NULL) {
|
|
HTREEITEM next = tree.GetNextVisibleItem(item);
|
|
if (next != NULL) {
|
|
CallSelectItem(tree, next);
|
|
if (IsKeyPressed(VK_SHIFT)) {
|
|
if (m_selStart == NULL) m_selStart = item;
|
|
SelectItemRange(tree, next);
|
|
}
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
break;
|
|
/*case VK_LEFT:
|
|
if (IsKeyPressed(VK_CONTROL)) {
|
|
tree.SendMessage(WM_HSCROLL, SB_LINEUP, 0);
|
|
}
|
|
break;
|
|
case VK_RIGHT:
|
|
if (IsKeyPressed(VK_CONTROL)) {
|
|
tree.SendMessage(WM_HSCROLL, SB_LINEDOWN, 0);
|
|
}
|
|
break;*/
|
|
}
|
|
return FALSE;
|
|
}
|
|
private:
|
|
LRESULT OnFocus(LPNMHDR hdr) {
|
|
if ( m_selection.size() > 100 ) {
|
|
CTreeViewCtrl tree(hdr->hwndFrom);
|
|
tree.RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_ERASE);
|
|
} else if (m_selection.size() > 0) {
|
|
CTreeViewCtrl tree(hdr->hwndFrom);
|
|
CRgn rgn; rgn.CreateRectRgn(0,0,0,0);
|
|
for(auto walk = m_selection.begin(); walk != m_selection.end(); ++walk) {
|
|
CRect rc;
|
|
if (tree.GetItemRect(*walk, rc, TRUE)) {
|
|
CRgn temp; temp.CreateRectRgnIndirect(rc);
|
|
rgn.CombineRgn(temp, RGN_OR);
|
|
}
|
|
}
|
|
tree.RedrawWindow(NULL, rgn, RDW_INVALIDATE | RDW_ERASE);
|
|
}
|
|
SetMsgHandled(FALSE);
|
|
return 0;
|
|
}
|
|
void CallSelectItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
const bool was = m_ownSelChange; m_ownSelChange = true;
|
|
tree.SelectItem(item);
|
|
m_ownSelChange = was;
|
|
}
|
|
LRESULT OnSelChangedFilter(LPNMHDR) {
|
|
if (m_ownSelChangeNotify) SetMsgHandled(FALSE);
|
|
return 0;
|
|
}
|
|
LRESULT OnItemDeleted(LPNMHDR pnmh) {
|
|
const HTREEITEM item = reinterpret_cast<NMTREEVIEW*>(pnmh)->itemOld.hItem;
|
|
m_selection.erase( item );
|
|
if (m_selStart == item) m_selStart = NULL;
|
|
SetMsgHandled(FALSE);
|
|
return 0;
|
|
}
|
|
LRESULT OnItemExpanded(LPNMHDR pnmh) {
|
|
NMTREEVIEW * info = reinterpret_cast<NMTREEVIEW *>(pnmh);
|
|
CTreeViewCtrl tree ( pnmh->hwndFrom );
|
|
if ((info->itemNew.state & TVIS_EXPANDED) == 0) {
|
|
if (DeselectChildren( tree, info->itemNew.hItem )) {
|
|
SendOnSelChanged(tree);
|
|
}
|
|
}
|
|
SetMsgHandled(FALSE);
|
|
return 0;
|
|
}
|
|
|
|
BOOL HandleClick(CTreeViewCtrl tree, CPoint pt) {
|
|
UINT htFlags = 0;
|
|
HTREEITEM item = tree.HitTest(pt, &htFlags);
|
|
if (item != NULL && (htFlags & TVHT_ONITEM) != 0) {
|
|
if (IsKeyPressed(VK_CONTROL)) {
|
|
SelectToggleItem(tree, item);
|
|
return TRUE;
|
|
} else if (item == tree.GetSelectedItem() && !IsItemSelected(item)) {
|
|
SelectToggleItem(tree, item);
|
|
return TRUE;
|
|
} else {
|
|
//tree.SelectItem(item);
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
LRESULT OnClick(LPNMHDR pnmh) {
|
|
CPoint pt(GetMessagePos());
|
|
CTreeViewCtrl tree ( pnmh->hwndFrom );
|
|
WIN32_OP_D ( tree.ScreenToClient( &pt ) );
|
|
return HandleClick(tree, pt) ? 1 : 0;
|
|
}
|
|
|
|
LRESULT OnSelChanging(LPNMHDR pnmh) {
|
|
if (!m_ownSelChange) {
|
|
//console::formatter() << "OnSelChanging";
|
|
NMTREEVIEW * info = reinterpret_cast<NMTREEVIEW *>(pnmh);
|
|
CTreeViewCtrl tree ( pnmh->hwndFrom );
|
|
const HTREEITEM item = info->itemNew.hItem;
|
|
|
|
if (IsTypingInProgress()) {
|
|
SelectSingleItem(tree, item);
|
|
} else if (IsKeyPressed(VK_SHIFT)) {
|
|
SelectItemRange(tree, item);
|
|
} else if (IsKeyPressed(VK_CONTROL)) {
|
|
SelectToggleItem(tree, item);
|
|
} else {
|
|
SelectSingleItem(tree, item);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void SelectItemRange(CTreeViewCtrl tree, HTREEITEM item) {
|
|
if (m_selStart == NULL || m_selStart == item) {
|
|
SelectSingleItem(tree, item);
|
|
return;
|
|
}
|
|
|
|
selection_t newSel = GrabRange(tree, m_selStart, item );
|
|
ApplySelection(tree, newSel);
|
|
}
|
|
static selection_t GrabRange(CTreeViewCtrl tree, HTREEITEM item1, HTREEITEM item2) {
|
|
selection_t range1, range2;
|
|
HTREEITEM walk1 = item1, walk2 = item2;
|
|
for(;;) {
|
|
if (walk1 != NULL) {
|
|
range1.insert( walk1 );
|
|
if (walk1 == item2) {
|
|
return range1;
|
|
}
|
|
walk1 = tree.GetNextVisibleItem(walk1);
|
|
}
|
|
if (walk2 != NULL) {
|
|
range2.insert( walk2 );
|
|
if (walk2 == item1) {
|
|
return range2;
|
|
}
|
|
walk2 = tree.GetNextVisibleItem(walk2);
|
|
}
|
|
if (walk1 == NULL && walk2 == NULL) {
|
|
// should not get here
|
|
return selection_t();
|
|
}
|
|
}
|
|
}
|
|
void SelectToggleItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
m_selStart = item;
|
|
if ( IsItemSelected( item ) ) {
|
|
m_selection.erase( item );
|
|
} else {
|
|
m_selection.insert( item );
|
|
}
|
|
UpdateItem(tree, item);
|
|
}
|
|
public:
|
|
void SelectSingleItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
m_selStart = item;
|
|
if (m_selection.size() == 1 && *m_selection.begin() == item) return;
|
|
DeselectAll(tree); SelectItem(tree, item);
|
|
}
|
|
|
|
void ApplySelection(CTreeViewCtrl tree, selection_t const & newSel) {
|
|
CRgn updateRgn;
|
|
bool changed = false;
|
|
if (newSel.size() != m_selection.size() && newSel.size() + m_selection.size() > 100) {
|
|
// don't bother with regions
|
|
changed = true;
|
|
} else {
|
|
WIN32_OP_D(updateRgn.CreateRectRgn(0, 0, 0, 0) != NULL);
|
|
for (auto walk = m_selection.begin(); walk != m_selection.end(); ++walk) {
|
|
if (newSel.count(*walk) == 0) {
|
|
changed = true;
|
|
CRect rc;
|
|
if (tree.GetItemRect(*walk, rc, TRUE)) {
|
|
CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc));
|
|
WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR);
|
|
}
|
|
}
|
|
}
|
|
for (auto walk = newSel.begin(); walk != newSel.end(); ++walk) {
|
|
if (m_selection.count(*walk) == 0) {
|
|
changed = true;
|
|
CRect rc;
|
|
if (tree.GetItemRect(*walk, rc, TRUE)) {
|
|
CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc));
|
|
WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (changed) {
|
|
m_selection = newSel;
|
|
tree.RedrawWindow(NULL, updateRgn);
|
|
SendOnSelChanged(tree);
|
|
}
|
|
}
|
|
|
|
void DeselectItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
if (IsItemSelected(item)) {
|
|
m_selection.erase(item); UpdateItem(tree, item);
|
|
}
|
|
}
|
|
void SelectItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
if (!IsItemSelected(item)) {
|
|
m_selection.insert(item); UpdateItem(tree, item);
|
|
}
|
|
}
|
|
|
|
void DeselectAll(CTreeViewCtrl tree) {
|
|
if (m_selection.size() == 0) return;
|
|
CRgn updateRgn;
|
|
if (m_selection.size() <= 100) {
|
|
WIN32_OP_D(updateRgn.CreateRectRgn(0, 0, 0, 0) != NULL);
|
|
for (auto walk = m_selection.begin(); walk != m_selection.end(); ++walk) {
|
|
CRect rc;
|
|
if (tree.GetItemRect(*walk, rc, TRUE)) {
|
|
CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc));
|
|
WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR);
|
|
}
|
|
}
|
|
}
|
|
m_selection.clear();
|
|
tree.RedrawWindow(NULL, updateRgn);
|
|
}
|
|
private:
|
|
void UpdateItem(CTreeViewCtrl tree, HTREEITEM item) {
|
|
CRect rc;
|
|
if (tree.GetItemRect(item, rc, TRUE) ) {
|
|
tree.RedrawWindow(rc);
|
|
}
|
|
SendOnSelChanged(tree);
|
|
}
|
|
void SendOnSelChanged(CTreeViewCtrl tree) {
|
|
NMHDR hdr = {};
|
|
hdr.code = TVN_SELCHANGED;
|
|
hdr.hwndFrom = tree;
|
|
hdr.idFrom = m_ID;
|
|
const bool was = m_ownSelChangeNotify; m_ownSelChangeNotify = true;
|
|
tree.GetParent().SendMessage(WM_NOTIFY, m_ID, (LPARAM) &hdr );
|
|
m_ownSelChangeNotify = was;
|
|
}
|
|
|
|
bool DeselectChildren( CTreeViewCtrl tree, HTREEITEM item ) {
|
|
bool state = false;
|
|
for(HTREEITEM walk = tree.GetChildItem( item ); walk != NULL; walk = tree.GetNextSiblingItem( walk ) ) {
|
|
if (m_selection.erase(walk) > 0) state = true;
|
|
if (m_selStart == walk) m_selStart = NULL;
|
|
if (tree.GetItemState( walk, TVIS_EXPANDED ) ) {
|
|
if (DeselectChildren( tree, walk )) state = true;
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
bool IsTypingInProgress() const {
|
|
return m_lastTypingTimeValid && (GetTickCount() - m_lastTypingTime < 500);
|
|
}
|
|
|
|
selection_t m_selection;
|
|
HTREEITEM m_selStart = NULL;
|
|
bool m_ownSelChangeNotify = false, m_ownSelChange = false;
|
|
DWORD m_lastTypingTime = 0; bool m_lastTypingTimeValid = false;
|
|
};
|