Mercurial > foo_out_sdl
comparison foosdk/sdk/libPPUI/TreeMultiSel.h @ 1:20d02a178406 default tip
*: check in everything else
yay
| author | Paper <paper@tflc.us> |
|---|---|
| date | Mon, 05 Jan 2026 02:15:46 -0500 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 0:e9bb126753e7 | 1:20d02a178406 |
|---|---|
| 1 #pragma once | |
| 2 | |
| 3 // ================================================================================ | |
| 4 // CTreeMultiSel | |
| 5 // Implementation of multi-selection in a tree view ctrl | |
| 6 // Instantiate with dialog ID of your treeview, | |
| 7 // plug into your dialog's message map. | |
| 8 // Doesn't work correctly with explorer-themed tree controls (glitches happen). | |
| 9 // ================================================================================ | |
| 10 | |
| 11 #include <set> | |
| 12 #include <vector> | |
| 13 | |
| 14 class CTreeMultiSel : public CMessageMap { | |
| 15 public: | |
| 16 typedef std::set<HTREEITEM> selection_t; | |
| 17 typedef std::vector<HTREEITEM> selectionOrdered_t; | |
| 18 | |
| 19 CTreeMultiSel(unsigned ID) : m_ID(ID) {} | |
| 20 | |
| 21 BEGIN_MSG_MAP_EX(CTreeMultiSel) | |
| 22 NOTIFY_HANDLER_EX(m_ID, TVN_ITEMEXPANDED, OnItemExpanded) | |
| 23 NOTIFY_HANDLER_EX(m_ID, NM_CLICK, OnClick) | |
| 24 NOTIFY_HANDLER_EX(m_ID, TVN_DELETEITEM, OnItemDeleted) | |
| 25 NOTIFY_HANDLER_EX(m_ID, TVN_SELCHANGING, OnSelChanging) | |
| 26 NOTIFY_HANDLER_EX(m_ID, TVN_SELCHANGED, OnSelChangedFilter) | |
| 27 NOTIFY_HANDLER_EX(m_ID, NM_SETFOCUS, OnFocus) | |
| 28 NOTIFY_HANDLER_EX(m_ID, NM_KILLFOCUS, OnFocus) | |
| 29 NOTIFY_HANDLER_EX(m_ID, NM_CUSTOMDRAW, OnCustomDraw) | |
| 30 END_MSG_MAP() | |
| 31 | |
| 32 const unsigned m_ID; | |
| 33 | |
| 34 // Retrieves selected items - on order of appearance in the view | |
| 35 selectionOrdered_t GetSelectionOrdered(CTreeViewCtrl tree) const { | |
| 36 HTREEITEM first = tree.GetRootItem(); | |
| 37 selectionOrdered_t ret; ret.reserve( m_selection.size() ); | |
| 38 for(HTREEITEM walk = first; walk != NULL; walk = tree.GetNextVisibleItem(walk)) { | |
| 39 if (m_selection.count(walk) > 0) ret.push_back( walk ); | |
| 40 } | |
| 41 return ret; | |
| 42 } | |
| 43 | |
| 44 //! Undefined order! Use only when order of selected items is not relevant. | |
| 45 selection_t GetSelection() const { return m_selection; } | |
| 46 selection_t const & GetSelectionRef() const { return m_selection; } | |
| 47 bool IsItemSelected(HTREEITEM item) const {return m_selection.count(item) > 0;} | |
| 48 size_t GetSelCount() const {return m_selection.size();} | |
| 49 //! Retrieves a single-selection item. Null if nothing or more than one item is selected. | |
| 50 HTREEITEM GetSingleSel() const { | |
| 51 if (m_selection.size() != 1) return NULL; | |
| 52 return *m_selection.begin(); | |
| 53 } | |
| 54 | |
| 55 void OnContextMenu_FixSelection(CTreeViewCtrl tree, CPoint pt) { | |
| 56 if (pt != CPoint(-1, -1)) { | |
| 57 WIN32_OP_D(tree.ScreenToClient(&pt)); | |
| 58 UINT flags = 0; | |
| 59 const HTREEITEM item = tree.HitTest(pt, &flags); | |
| 60 if (item != NULL && (flags & TVHT_ONITEM) != 0) { | |
| 61 if (!IsItemSelected(item)) { | |
| 62 SelectSingleItem(tree, item); | |
| 63 } | |
| 64 CallSelectItem(tree, item); | |
| 65 } | |
| 66 } | |
| 67 } | |
| 68 | |
| 69 void OnLButtonDown(CTreeViewCtrl tree, WPARAM wp, LPARAM lp) { | |
| 70 if (!IsKeyPressed(VK_CONTROL)) { | |
| 71 UINT flags = 0; | |
| 72 HTREEITEM item = tree.HitTest(CPoint(lp), &flags); | |
| 73 if (item != NULL && (flags & TVHT_ONITEM) != 0) { | |
| 74 if (!IsItemSelected(item)) tree.SelectItem(item); | |
| 75 } | |
| 76 } | |
| 77 } | |
| 78 static bool IsNavKey(UINT vk) { | |
| 79 switch(vk) { | |
| 80 case VK_UP: | |
| 81 case VK_DOWN: | |
| 82 case VK_RIGHT: | |
| 83 case VK_LEFT: | |
| 84 case VK_PRIOR: | |
| 85 case VK_NEXT: | |
| 86 case VK_HOME: | |
| 87 case VK_END: | |
| 88 return true; | |
| 89 default: | |
| 90 return false; | |
| 91 } | |
| 92 } | |
| 93 BOOL OnChar(CTreeViewCtrl tree, WPARAM code) { | |
| 94 switch(code) { | |
| 95 case ' ': | |
| 96 if (IsKeyPressed(VK_CONTROL) || !IsTypingInProgress()) { | |
| 97 HTREEITEM item = tree.GetSelectedItem(); | |
| 98 if (item != NULL) SelectToggleItem(tree, item); | |
| 99 return TRUE; | |
| 100 } | |
| 101 break; | |
| 102 } | |
| 103 m_lastTypingTime = GetTickCount(); m_lastTypingTimeValid = true; | |
| 104 return FALSE; | |
| 105 } | |
| 106 BOOL OnKeyDown(CTreeViewCtrl tree, UINT vKey) { | |
| 107 if (IsNavKey(vKey)) m_lastTypingTimeValid = false; | |
| 108 switch(vKey) { | |
| 109 case VK_UP: | |
| 110 if (IsKeyPressed(VK_CONTROL)) { | |
| 111 HTREEITEM item = tree.GetSelectedItem(); | |
| 112 if (item != NULL) { | |
| 113 HTREEITEM prev = tree.GetPrevVisibleItem(item); | |
| 114 if (prev != NULL) { | |
| 115 CallSelectItem(tree, prev); | |
| 116 if (IsKeyPressed(VK_SHIFT)) { | |
| 117 if (m_selStart == NULL) m_selStart = item; | |
| 118 SelectItemRange(tree, prev); | |
| 119 } | |
| 120 } | |
| 121 } | |
| 122 return TRUE; | |
| 123 } | |
| 124 break; | |
| 125 case VK_DOWN: | |
| 126 if (IsKeyPressed(VK_CONTROL)) { | |
| 127 HTREEITEM item = tree.GetSelectedItem(); | |
| 128 if (item != NULL) { | |
| 129 HTREEITEM next = tree.GetNextVisibleItem(item); | |
| 130 if (next != NULL) { | |
| 131 CallSelectItem(tree, next); | |
| 132 if (IsKeyPressed(VK_SHIFT)) { | |
| 133 if (m_selStart == NULL) m_selStart = item; | |
| 134 SelectItemRange(tree, next); | |
| 135 } | |
| 136 } | |
| 137 } | |
| 138 return TRUE; | |
| 139 } | |
| 140 break; | |
| 141 /*case VK_LEFT: | |
| 142 if (IsKeyPressed(VK_CONTROL)) { | |
| 143 tree.SendMessage(WM_HSCROLL, SB_LINEUP, 0); | |
| 144 } | |
| 145 break; | |
| 146 case VK_RIGHT: | |
| 147 if (IsKeyPressed(VK_CONTROL)) { | |
| 148 tree.SendMessage(WM_HSCROLL, SB_LINEDOWN, 0); | |
| 149 } | |
| 150 break;*/ | |
| 151 } | |
| 152 return FALSE; | |
| 153 } | |
| 154 private: | |
| 155 LRESULT OnFocus(LPNMHDR hdr) { | |
| 156 if ( m_selection.size() > 100 ) { | |
| 157 CTreeViewCtrl tree(hdr->hwndFrom); | |
| 158 tree.RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_ERASE); | |
| 159 } else if (m_selection.size() > 0) { | |
| 160 CTreeViewCtrl tree(hdr->hwndFrom); | |
| 161 CRgn rgn; rgn.CreateRectRgn(0,0,0,0); | |
| 162 for(auto walk : m_selection) { | |
| 163 CRect rc; | |
| 164 if (tree.GetItemRect(walk, rc, TRUE)) { | |
| 165 CRgn temp; temp.CreateRectRgnIndirect(rc); | |
| 166 rgn.CombineRgn(temp, RGN_OR); | |
| 167 } | |
| 168 } | |
| 169 tree.RedrawWindow(NULL, rgn, RDW_INVALIDATE | RDW_ERASE); | |
| 170 } | |
| 171 SetMsgHandled(FALSE); | |
| 172 return 0; | |
| 173 } | |
| 174 void CallSelectItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 175 const bool was = m_ownSelChange; m_ownSelChange = true; | |
| 176 tree.SelectItem(item); | |
| 177 m_ownSelChange = was; | |
| 178 } | |
| 179 LRESULT OnSelChangedFilter(LPNMHDR) { | |
| 180 if (m_ownSelChangeNotify) SetMsgHandled(FALSE); | |
| 181 return 0; | |
| 182 } | |
| 183 LRESULT OnItemDeleted(LPNMHDR pnmh) { | |
| 184 const HTREEITEM item = reinterpret_cast<NMTREEVIEW*>(pnmh)->itemOld.hItem; | |
| 185 m_selection.erase( item ); | |
| 186 if (m_selStart == item) m_selStart = NULL; | |
| 187 SetMsgHandled(FALSE); | |
| 188 return 0; | |
| 189 } | |
| 190 LRESULT OnItemExpanded(LPNMHDR pnmh) { | |
| 191 NMTREEVIEW * info = reinterpret_cast<NMTREEVIEW *>(pnmh); | |
| 192 CTreeViewCtrl tree ( pnmh->hwndFrom ); | |
| 193 if ((info->itemNew.state & TVIS_EXPANDED) == 0) { | |
| 194 if (DeselectChildren( tree, info->itemNew.hItem )) { | |
| 195 SendOnSelChanged(tree); | |
| 196 } | |
| 197 } | |
| 198 SetMsgHandled(FALSE); | |
| 199 return 0; | |
| 200 } | |
| 201 | |
| 202 void FixFocusItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 203 if (this->IsItemSelected(item) || tree.GetSelectedItem() != item) return; | |
| 204 | |
| 205 auto scope = pfc::autoToggle(m_ownSelChange, true); | |
| 206 | |
| 207 for(;;) { | |
| 208 if (item == TVI_ROOT || item == NULL || this->IsItemSelected(item)) { | |
| 209 tree.SelectItem(item); return; | |
| 210 } | |
| 211 for (auto walk = tree.GetPrevSiblingItem(item); walk != NULL; walk = tree.GetPrevSiblingItem(walk)) { | |
| 212 if (this->IsItemSelected(walk)) { | |
| 213 tree.SelectItem(walk); return; | |
| 214 } | |
| 215 } | |
| 216 for (auto walk = tree.GetNextSiblingItem(item); walk != NULL; walk = tree.GetNextSiblingItem(walk)) { | |
| 217 if (this->IsItemSelected(walk)) { | |
| 218 tree.SelectItem(walk); return; | |
| 219 } | |
| 220 } | |
| 221 item = tree.GetParentItem(item); | |
| 222 } | |
| 223 } | |
| 224 | |
| 225 BOOL HandleClick(CTreeViewCtrl tree, CPoint pt) { | |
| 226 UINT htFlags = 0; | |
| 227 HTREEITEM item = tree.HitTest(pt, &htFlags); | |
| 228 if (item != NULL && (htFlags & TVHT_ONITEM) != 0) { | |
| 229 if (IsKeyPressed(VK_CONTROL)) { | |
| 230 SelectToggleItem(tree, item); | |
| 231 FixFocusItem(tree, item); | |
| 232 return TRUE; | |
| 233 } else if (item == tree.GetSelectedItem() && !IsItemSelected(item)) { | |
| 234 SelectToggleItem(tree, item); | |
| 235 return TRUE; | |
| 236 } else { | |
| 237 //tree.SelectItem(item); | |
| 238 return FALSE; | |
| 239 } | |
| 240 } else { | |
| 241 return FALSE; | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 LRESULT OnClick(LPNMHDR pnmh) { | |
| 246 CPoint pt(GetMessagePos()); | |
| 247 CTreeViewCtrl tree ( pnmh->hwndFrom ); | |
| 248 WIN32_OP_D ( tree.ScreenToClient( &pt ) ); | |
| 249 return HandleClick(tree, pt) ? 1 : 0; | |
| 250 } | |
| 251 | |
| 252 LRESULT OnSelChanging(LPNMHDR pnmh) { | |
| 253 if (!m_ownSelChange) { | |
| 254 //console::formatter() << "OnSelChanging"; | |
| 255 NMTREEVIEW * info = reinterpret_cast<NMTREEVIEW *>(pnmh); | |
| 256 CTreeViewCtrl tree ( pnmh->hwndFrom ); | |
| 257 const HTREEITEM item = info->itemNew.hItem; | |
| 258 | |
| 259 if (IsTypingInProgress()) { | |
| 260 SelectSingleItem(tree, item); | |
| 261 } else if (IsKeyPressed(VK_SHIFT)) { | |
| 262 SelectItemRange(tree, item); | |
| 263 } else if (IsKeyPressed(VK_CONTROL)) { | |
| 264 SelectToggleItem(tree, item); | |
| 265 } else { | |
| 266 SelectSingleItem(tree, item); | |
| 267 } | |
| 268 } | |
| 269 return 0; | |
| 270 } | |
| 271 | |
| 272 void SelectItemRange(CTreeViewCtrl tree, HTREEITEM item) { | |
| 273 if (m_selStart == NULL || m_selStart == item) { | |
| 274 SelectSingleItem(tree, item); | |
| 275 return; | |
| 276 } | |
| 277 | |
| 278 selection_t newSel = GrabRange(tree, m_selStart, item ); | |
| 279 ApplySelection(tree, std::move(newSel)); | |
| 280 } | |
| 281 static selection_t GrabRange(CTreeViewCtrl tree, HTREEITEM item1, HTREEITEM item2) { | |
| 282 selection_t range1, range2; | |
| 283 HTREEITEM walk1 = item1, walk2 = item2; | |
| 284 for(;;) { | |
| 285 if (walk1 != NULL) { | |
| 286 range1.insert( walk1 ); | |
| 287 if (walk1 == item2) { | |
| 288 return range1; | |
| 289 } | |
| 290 walk1 = tree.GetNextVisibleItem(walk1); | |
| 291 } | |
| 292 if (walk2 != NULL) { | |
| 293 range2.insert( walk2 ); | |
| 294 if (walk2 == item1) { | |
| 295 return range2; | |
| 296 } | |
| 297 walk2 = tree.GetNextVisibleItem(walk2); | |
| 298 } | |
| 299 if (walk1 == NULL && walk2 == NULL) { | |
| 300 // should not get here | |
| 301 return selection_t(); | |
| 302 } | |
| 303 } | |
| 304 } | |
| 305 void SelectToggleItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 306 m_selStart = item; | |
| 307 if ( IsItemSelected( item ) ) { | |
| 308 m_selection.erase( item ); | |
| 309 } else { | |
| 310 m_selection.insert( item ); | |
| 311 } | |
| 312 UpdateItem(tree, item); | |
| 313 } | |
| 314 | |
| 315 LRESULT OnCustomDraw(LPNMHDR hdr) { | |
| 316 NMTVCUSTOMDRAW* info = (NMTVCUSTOMDRAW*)hdr; | |
| 317 switch (info->nmcd.dwDrawStage) { | |
| 318 case CDDS_ITEMPREPAINT: | |
| 319 // NOTE: This doesn't work all the way. Unflagging CDIS_FOCUS isn't respected, causing weird behaviors when using ctrl+cursors or unselecting items. | |
| 320 if (this->IsItemSelected((HTREEITEM)info->nmcd.dwItemSpec)) { | |
| 321 info->nmcd.uItemState |= CDIS_SELECTED; | |
| 322 } else { | |
| 323 info->nmcd.uItemState &= ~(CDIS_FOCUS | CDIS_SELECTED); | |
| 324 } | |
| 325 return CDRF_DODEFAULT; | |
| 326 case CDDS_PREPAINT: | |
| 327 return CDRF_NOTIFYITEMDRAW; | |
| 328 default: | |
| 329 return CDRF_DODEFAULT; | |
| 330 } | |
| 331 } | |
| 332 public: | |
| 333 void SelectSingleItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 334 m_selStart = item; | |
| 335 if (m_selection.size() == 1 && *m_selection.begin() == item) return; | |
| 336 DeselectAll(tree); SelectItem(tree, item); | |
| 337 } | |
| 338 | |
| 339 void ApplySelection(CTreeViewCtrl tree, selection_t && newSel) { | |
| 340 CRgn updateRgn; | |
| 341 bool changed = false; | |
| 342 if (newSel.size() != m_selection.size() && newSel.size() + m_selection.size() > 100) { | |
| 343 // don't bother with regions | |
| 344 changed = true; | |
| 345 } else { | |
| 346 WIN32_OP_D(updateRgn.CreateRectRgn(0, 0, 0, 0) != NULL); | |
| 347 for (auto walk : m_selection) { | |
| 348 if (newSel.count(walk) == 0) { | |
| 349 changed = true; | |
| 350 CRect rc; | |
| 351 if (tree.GetItemRect(walk, rc, TRUE)) { | |
| 352 CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc)); | |
| 353 WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR); | |
| 354 } | |
| 355 } | |
| 356 } | |
| 357 for (auto walk : newSel) { | |
| 358 if (m_selection.count(walk) == 0) { | |
| 359 changed = true; | |
| 360 CRect rc; | |
| 361 if (tree.GetItemRect(walk, rc, TRUE)) { | |
| 362 CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc)); | |
| 363 WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR); | |
| 364 } | |
| 365 } | |
| 366 } | |
| 367 } | |
| 368 if (changed) { | |
| 369 m_selection = std::move(newSel); | |
| 370 tree.RedrawWindow(NULL, updateRgn); | |
| 371 SendOnSelChanged(tree); | |
| 372 } | |
| 373 } | |
| 374 | |
| 375 void DeselectItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 376 if (IsItemSelected(item)) { | |
| 377 m_selection.erase(item); UpdateItem(tree, item); | |
| 378 } | |
| 379 } | |
| 380 void SelectItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 381 if (!IsItemSelected(item)) { | |
| 382 m_selection.insert(item); UpdateItem(tree, item); | |
| 383 } | |
| 384 } | |
| 385 | |
| 386 void DeselectAll(CTreeViewCtrl tree) { | |
| 387 if (m_selection.size() == 0) return; | |
| 388 CRgn updateRgn; | |
| 389 if (m_selection.size() <= 100) { | |
| 390 WIN32_OP_D(updateRgn.CreateRectRgn(0, 0, 0, 0) != NULL); | |
| 391 for (auto walk : m_selection) { | |
| 392 CRect rc; | |
| 393 if (tree.GetItemRect(walk, rc, TRUE)) { | |
| 394 CRgn temp; WIN32_OP_D(temp.CreateRectRgnIndirect(rc)); | |
| 395 WIN32_OP_D(updateRgn.CombineRgn(temp, RGN_OR) != ERROR); | |
| 396 } | |
| 397 } | |
| 398 } | |
| 399 m_selection.clear(); | |
| 400 tree.RedrawWindow(NULL, updateRgn); | |
| 401 } | |
| 402 private: | |
| 403 void UpdateItem(CTreeViewCtrl tree, HTREEITEM item) { | |
| 404 CRect rc; | |
| 405 if (tree.GetItemRect(item, rc, TRUE) ) { | |
| 406 tree.RedrawWindow(rc); | |
| 407 } | |
| 408 SendOnSelChanged(tree); | |
| 409 } | |
| 410 void SendOnSelChanged(CTreeViewCtrl tree) { | |
| 411 NMHDR hdr = {}; | |
| 412 hdr.code = TVN_SELCHANGED; | |
| 413 hdr.hwndFrom = tree; | |
| 414 hdr.idFrom = m_ID; | |
| 415 const bool was = m_ownSelChangeNotify; m_ownSelChangeNotify = true; | |
| 416 tree.GetParent().SendMessage(WM_NOTIFY, m_ID, (LPARAM) &hdr ); | |
| 417 m_ownSelChangeNotify = was; | |
| 418 } | |
| 419 | |
| 420 bool DeselectChildren( CTreeViewCtrl tree, HTREEITEM item ) { | |
| 421 bool state = false; | |
| 422 for(HTREEITEM walk = tree.GetChildItem( item ); walk != NULL; walk = tree.GetNextSiblingItem( walk ) ) { | |
| 423 if (m_selection.erase(walk) > 0) state = true; | |
| 424 if (m_selStart == walk) m_selStart = NULL; | |
| 425 if (tree.GetItemState( walk, TVIS_EXPANDED ) ) { | |
| 426 if (DeselectChildren( tree, walk )) state = true; | |
| 427 } | |
| 428 } | |
| 429 return state; | |
| 430 } | |
| 431 | |
| 432 bool IsTypingInProgress() const { | |
| 433 return m_lastTypingTimeValid && (GetTickCount() - m_lastTypingTime < 500); | |
| 434 } | |
| 435 | |
| 436 selection_t m_selection; | |
| 437 HTREEITEM m_selStart = NULL; | |
| 438 bool m_ownSelChangeNotify = false, m_ownSelChange = false; | |
| 439 DWORD m_lastTypingTime = 0; bool m_lastTypingTimeValid = false; | |
| 440 }; |
