listview.cpp (17631B)
1 /* ReaPack: Package manager for REAPER 2 * Copyright (C) 2015-2025 Christian Fillion 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "listview.hpp" 19 20 #include "iconlist.hpp" 21 #include "menu.hpp" 22 #include "time.hpp" 23 #include "version.hpp" 24 #include "win32.hpp" 25 26 #include <boost/algorithm/string/case_conv.hpp> 27 #include <cassert> 28 #include <reaper_plugin_secrets.h> 29 30 static int adjustWidth(const int points) 31 { 32 #ifdef _WIN32 33 if(points < 1) 34 return points; 35 else // magic number to make pretty sizes... 36 return static_cast<int>(std::ceil(points * 0.863)); 37 #else 38 return points; 39 #endif 40 } 41 42 ListView::ListView(HWND handle, const Columns &columns) 43 : Control(handle), m_dirty(0), m_customizable(false), m_sort(), m_defaultSort() 44 { 45 for(const Column &col : columns) 46 addColumn(col); 47 48 int style = LVS_EX_FULLROWSELECT; 49 50 #ifdef LVS_EX_LABELTIP 51 // unsupported by SWELL, but always enabled on macOS anyway 52 style |= LVS_EX_LABELTIP; 53 #endif 54 55 #ifdef LVS_EX_DOUBLEBUFFER 56 style |= LVS_EX_DOUBLEBUFFER; 57 #endif 58 59 setExStyle(style); 60 } 61 62 void ListView::setExStyle(const int style, const bool enable) 63 { 64 ListView_SetExtendedListViewStyleEx(handle(), style, enable ? style : 0); 65 } 66 67 int ListView::addColumn(const Column &col) 68 { 69 assert(m_rows.empty()); 70 71 LVCOLUMN item{}; 72 73 item.mask |= LVCF_WIDTH; 74 item.cx = col.test(CollapseFlag) ? 0 : adjustWidth(col.width); 75 76 const auto &&label = Win32::widen(col.label); 77 if(!col.test(NoLabelFlag)) { 78 item.mask |= LVCF_TEXT; 79 item.pszText = const_cast<Win32::char_type *>(label.c_str()); 80 } 81 82 const int index = columnCount(); 83 ListView_InsertColumn(handle(), index, &item); 84 m_cols.push_back(col); 85 86 if(m_sort && m_sort->column == index) 87 setSortArrow(true); 88 89 return index; 90 } 91 92 ListView::Row *ListView::createRow(void *data) 93 { 94 const int index = rowCount(); 95 insertItem(index, index); 96 97 return m_rows.emplace_back(std::make_unique<Row>(data, this)).get(); 98 } 99 100 void ListView::insertItem(const int viewIndex, const int rowIndex) 101 { 102 LVITEM item{}; 103 item.iItem = viewIndex; 104 105 item.mask |= LVIF_PARAM; 106 item.lParam = rowIndex; 107 108 ListView_InsertItem(handle(), &item); 109 } 110 111 void ListView::updateCell(int row, int cell) 112 { 113 const int viewRowIndex = translate(row); 114 const auto &&text = Win32::widen(m_rows[row]->cell(cell).value); 115 116 ListView_SetItemText(handle(), viewRowIndex, cell, 117 const_cast<Win32::char_type *>(text.c_str())); 118 119 if(m_sort && m_sort->column == cell) 120 m_dirty |= NeedSortFlag; 121 122 m_dirty |= NeedFilterFlag; 123 } 124 125 void ListView::enableIcons() 126 { 127 static IconList list({IconList::UncheckedIcon, IconList::CheckedIcon}); 128 129 // NOTE: the list must have the LVS_SHAREIMAGELISTS style to prevent 130 // it from taking ownership of the image list 131 ListView_SetImageList(handle(), list.handle(), LVSIL_SMALL); 132 } 133 134 void ListView::setRowIcon(const int row, const int image) 135 { 136 LVITEM item{}; 137 item.iItem = translate(row); 138 item.iImage = image; 139 item.mask |= LVIF_IMAGE; 140 141 ListView_SetItem(handle(), &item); 142 } 143 144 void ListView::removeRow(const int userIndex) 145 { 146 // translate to view index before fixing lParams 147 const int viewIndex = translate(userIndex); 148 149 // shift lParam and userIndex of subsequent rows to reflect the new indexes 150 const int size = rowCount(); 151 for(int i = userIndex + 1; i < size; i++) { 152 m_rows[i]->userIndex = i - 1; 153 154 LVITEM item{}; 155 item.iItem = translate(i); 156 item.mask |= LVIF_PARAM; 157 item.lParam = m_rows[i]->userIndex; 158 ListView_SetItem(handle(), &item); 159 } 160 161 ListView_DeleteItem(handle(), viewIndex); 162 m_rows.erase(m_rows.begin() + userIndex); 163 164 reindexVisible(); // do it now so further removeRow will work as expected 165 } 166 167 void ListView::resizeColumn(const int index, const int width) 168 { 169 ListView_SetColumnWidth(handle(), index, adjustWidth(width)); 170 } 171 172 int ListView::columnWidth(const int index) const 173 { 174 return ListView_GetColumnWidth(handle(), index); 175 } 176 177 int ListView::columnCount() const 178 { 179 #ifdef __GNUC__ 180 // workaround for Walloc-size-larger-than in GCC 14 when LTO is enabled 181 if(m_cols.size() > INT_MAX) 182 __builtin_unreachable(); 183 #endif 184 185 return static_cast<int>(m_cols.size()); 186 } 187 188 void ListView::sort() 189 { 190 static const auto compare = [](LPARAM aRow, LPARAM bRow, LPARAM param) 191 { 192 const int indexDiff = static_cast<int>(aRow - bRow); 193 194 ListView *view = reinterpret_cast<ListView *>(param); 195 196 if(!view->m_sort) 197 return indexDiff; 198 199 const int columnIndex = view->m_sort->column; 200 const Column &column = view->m_cols[columnIndex]; 201 202 int ret = column.compare(view->row(aRow)->cell(columnIndex), 203 view->row(bRow)->cell(columnIndex)); 204 205 if(view->m_sort->order == DescendingOrder) 206 ret = -ret; 207 208 return ret ? ret : indexDiff; 209 }; 210 211 ListView_SortItems(handle(), compare, reinterpret_cast<LPARAM>(this)); 212 213 m_dirty = (m_dirty | NeedReindexFlag) & ~NeedSortFlag; 214 } 215 216 void ListView::sortByColumn(const int index, const SortOrder order, const bool user) 217 { 218 if(m_sort) 219 setSortArrow(false); 220 221 const Sort settings{index, order}; 222 223 if(!user) 224 m_defaultSort = settings; 225 226 m_sort = settings; 227 m_dirty |= NeedSortFlag; 228 229 setSortArrow(true); 230 } 231 232 void ListView::setSortArrow(const bool set) 233 { 234 if(!m_sort) 235 return; 236 237 HWND header = ListView_GetHeader(handle()); 238 239 HDITEM item{}; 240 item.mask |= HDI_FORMAT; 241 242 if(!Header_GetItem(header, m_sort->column, &item)) 243 return; 244 245 item.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP); // clear 246 247 if(set) { 248 switch(m_sort->order) { 249 case AscendingOrder: 250 item.fmt |= HDF_SORTUP; 251 break; 252 case DescendingOrder: 253 item.fmt |= HDF_SORTDOWN; 254 } 255 } 256 257 Header_SetItem(header, m_sort->column, &item); 258 } 259 260 void ListView::filter() 261 { 262 std::vector<int> hide; 263 264 for(int ri = 0; ri < rowCount(); ++ri) { 265 const auto &row = m_rows[ri]; 266 267 if(m_filter.match(row->filterValues())) { 268 if(row->viewIndex == -1) { 269 row->viewIndex = visibleRowCount(); 270 insertItem(row->viewIndex, ri); 271 272 for(int ci = 0; ci < columnCount(); ++ci) 273 updateCell(ri, ci); 274 275 m_dirty |= NeedSortFlag; 276 } 277 } 278 else if(row->viewIndex > -1) { 279 hide.emplace_back(row->viewIndex); 280 row->viewIndex = -1; 281 } 282 } 283 284 std::sort(hide.begin(), hide.end()); 285 for(int i = 0; i < static_cast<int>(hide.size()); ++i) { 286 ListView_DeleteItem(handle(), hide[i] - i); 287 m_dirty |= NeedReindexFlag; 288 } 289 290 m_dirty &= ~NeedFilterFlag; 291 } 292 293 void ListView::setFilter(const std::string &newFilter) 294 { 295 m_filter = newFilter; 296 m_dirty |= NeedFilterFlag; 297 } 298 299 void ListView::reindexVisible() 300 { 301 const int visibleCount = visibleRowCount(); 302 for(int viewIndex = 0; viewIndex < visibleCount; viewIndex++) { 303 LVITEM item{}; 304 item.iItem = viewIndex; 305 item.mask |= LVIF_PARAM; 306 ListView_GetItem(handle(), &item); 307 308 row(item.lParam)->viewIndex = viewIndex; 309 } 310 311 m_dirty &= ~NeedReindexFlag; 312 } 313 314 void ListView::endEdit() 315 { 316 if(m_dirty & NeedFilterFlag) 317 filter(); // filter may set NeedSortFlag 318 if(m_dirty & NeedSortFlag) 319 sort(); // sort may set NeedReindexFlag 320 if(m_dirty & NeedReindexFlag) 321 reindexVisible(); 322 323 assert(!m_dirty); 324 } 325 326 void ListView::clear() 327 { 328 ListView_DeleteAllItems(handle()); 329 #ifdef __APPLE__ 330 // NSTableView preverves the previous selection when removing rows after 331 // beginUpdates is called (via WM_SETREDRAW=0 in SWELL) 332 unselectAll(); 333 #endif 334 335 m_rows.clear(); 336 } 337 338 void ListView::reset() 339 { 340 clear(); 341 342 for(int i = columnCount(); i > 0; i--) 343 ListView_DeleteColumn(handle(), i - 1); 344 345 m_cols.clear(); 346 347 m_customizable = false; 348 m_sort = std::nullopt; 349 m_defaultSort = std::nullopt; 350 } 351 352 void ListView::setSelected(const int index, const bool select) 353 { 354 ListView_SetItemState(handle(), translate(index), 355 select ? LVIS_SELECTED : 0, LVIS_SELECTED); 356 } 357 358 void ListView::selectAll() 359 { 360 InhibitControl inhibit(this); 361 select(-1); 362 } 363 364 void ListView::unselectAll() 365 { 366 InhibitControl inhibit(this); 367 unselect(-1); 368 } 369 370 int ListView::visibleRowCount() const 371 { 372 return ListView_GetItemCount(handle()); 373 } 374 375 int ListView::currentIndex() const 376 { 377 const int internalIndex = ListView_GetNextItem(handle(), -1, LVNI_SELECTED); 378 379 if(internalIndex < 0) 380 return -1; 381 else 382 return translateBack(internalIndex); 383 } 384 385 std::vector<int> ListView::selection(const bool sort) const 386 { 387 int index = -1; 388 std::vector<int> selectedIndexes; 389 390 while((index = ListView_GetNextItem(handle(), index, LVNI_SELECTED)) != -1) 391 selectedIndexes.push_back(translateBack(index)); 392 393 if(sort) 394 std::sort(selectedIndexes.begin(), selectedIndexes.end()); 395 396 return selectedIndexes; 397 } 398 399 int ListView::selectionSize() const 400 { 401 return ListView_GetSelectedCount(handle()); 402 } 403 404 bool ListView::headerHitTest(const int x, const int y) const 405 { 406 #ifdef _WIN32 407 RECT rect; 408 GetWindowRect(ListView_GetHeader(handle()), &rect); 409 410 const int headerHeight = rect.bottom - rect.top; 411 #elif !defined(__APPLE__) 412 const int headerHeight = SWELL_GetListViewHeaderHeight(handle()); 413 #endif 414 415 POINT point{x, y}; 416 ScreenToClient(handle(), &point); 417 418 #ifdef __APPLE__ 419 // This was broken on Linux and used a hard-coded header height on Windows 420 // Fixed in REAPER v6.03 421 return ListView_HeaderHitTest(handle(), point); 422 #else 423 return point.y <= headerHeight; 424 #endif 425 } 426 427 int ListView::itemUnder(const int x, const int y, bool *overIcon) const 428 { 429 LVHITTESTINFO test{{x, y}}; 430 ScreenToClient(handle(), &test.pt); 431 ListView_SubItemHitTest(handle(), &test); 432 433 if(overIcon) { 434 *overIcon = test.iSubItem == 0 && 435 (test.flags & (LVHT_ONITEMICON | LVHT_ONITEMSTATEICON)) != 0 && 436 (test.flags & LVHT_ONITEMLABEL) == 0; 437 } 438 439 return translateBack(test.iItem); 440 } 441 442 int ListView::itemUnderMouse(bool *overIcon) const 443 { 444 POINT point; 445 GetCursorPos(&point); 446 return itemUnder(point.x, point.y, overIcon); 447 } 448 449 int ListView::scroll() const 450 { 451 return ListView_GetTopIndex(handle()); 452 } 453 454 void ListView::setScroll(const int index) 455 { 456 #ifdef ListView_GetItemPosition 457 if(index < 0) 458 return; 459 460 RECT rect; 461 ListView_GetViewRect(handle(), &rect); 462 463 POINT itemPos{}; 464 if(ListView_GetItemPosition(handle(), index - 1, &itemPos)) 465 ListView_Scroll(handle(), abs(rect.left) + itemPos.x, abs(rect.top) + itemPos.y); 466 #endif 467 } 468 469 void ListView::autoSizeHeader() 470 { 471 #ifdef LVSCW_AUTOSIZE_USEHEADER 472 resizeColumn(columnCount() - 1, LVSCW_AUTOSIZE_USEHEADER); 473 #endif 474 } 475 476 void ListView::onNotify(LPNMHDR info, LPARAM lParam) 477 { 478 switch(info->code) { 479 case LVN_ITEMCHANGED: 480 onItemChanged(lParam); 481 break; 482 case NM_CLICK: 483 case NM_DBLCLK: 484 onClick(info->code == NM_DBLCLK); 485 break; 486 case LVN_COLUMNCLICK: 487 onColumnClick(lParam); 488 break; 489 }; 490 } 491 492 bool ListView::onContextMenu(HWND dialog, int x, int y) 493 { 494 SetFocus(handle()); 495 496 const bool keyboardTrigger = x == -1 && y == -1; 497 498 if(!keyboardTrigger && headerHitTest(x, y)) { 499 if(m_customizable) // show menu only if header is customizable 500 headerMenu(x, y); 501 return true; 502 } 503 504 #ifdef ListView_GetItemPosition // unsuported by SWELL 505 int index; 506 507 // adjust the context menu's position when using Shift+F10 on Windows 508 if(keyboardTrigger) { 509 index = currentIndex(); 510 511 // find the location of the current item or of the first item 512 POINT itemPos{}; 513 ListView_GetItemPosition(handle(), translate(std::max(0, index)), &itemPos); 514 ClientToScreen(handle(), &itemPos); 515 516 RECT controlRect; 517 GetWindowRect(handle(), &controlRect); 518 519 x = std::max(controlRect.left, std::min(itemPos.x, controlRect.right)); 520 y = std::max(controlRect.top, std::min(itemPos.y, controlRect.bottom)); 521 } 522 else 523 index = itemUnder(x, y); 524 #else 525 const int index = itemUnder(x, y); 526 #endif 527 528 Menu menu; 529 530 if(!onFillContextMenu(menu, index).value_or(false)) 531 return false; 532 533 menu.show(x, y, dialog); 534 535 return true; 536 } 537 538 void ListView::onItemChanged(const LPARAM lParam) 539 { 540 #ifdef _WIN32 541 const auto info = reinterpret_cast<LPNMLISTVIEW>(lParam); 542 543 if(info->uChanged & LVIF_STATE) 544 #endif 545 onSelect(); 546 } 547 548 void ListView::onClick(const bool dbclick) 549 { 550 bool overIcon; 551 552 if(itemUnderMouse(&overIcon) > -1 && currentIndex() > -1) { 553 if(dbclick) 554 onActivate(); 555 else if(overIcon) 556 onIconClick(); 557 } 558 } 559 560 void ListView::onColumnClick(const LPARAM lParam) 561 { 562 const auto info = reinterpret_cast<LPNMLISTVIEW>(lParam); 563 const int col = info->iSubItem; 564 SortOrder order = AscendingOrder; 565 566 if(m_sort && col == m_sort->column) { 567 switch(m_sort->order) { 568 case AscendingOrder: 569 order = DescendingOrder; 570 break; 571 case DescendingOrder: 572 order = AscendingOrder; 573 break; 574 } 575 } 576 577 sortByColumn(col, order, true); 578 endEdit(); 579 } 580 581 int ListView::translate(const int userIndex) const 582 { 583 if(!m_sort || userIndex < 0) 584 return userIndex; 585 else 586 return row(userIndex)->viewIndex; 587 } 588 589 int ListView::translateBack(const int internalIndex) const 590 { 591 if(!m_sort || internalIndex < 0) 592 return internalIndex; 593 594 LVITEM item{}; 595 item.iItem = internalIndex; 596 item.mask |= LVIF_PARAM; 597 598 if(ListView_GetItem(handle(), &item)) 599 return static_cast<int>(item.lParam); 600 else 601 return -1; 602 } 603 604 void ListView::headerMenu(const int x, const int y) 605 { 606 enum { ACTION_RESTORE = 800 }; 607 608 Menu menu; 609 menu.disable(menu.addAction("Visible columns:", 0)); 610 611 for(int i = 0; i < columnCount(); i++) { 612 const auto id = menu.addAction(m_cols[i].label.c_str(), i | (1 << 8)); 613 614 if(columnWidth(i)) 615 menu.check(id); 616 } 617 618 menu.addSeparator(); 619 menu.addAction("Reset columns", ACTION_RESTORE); 620 621 const int cmd = menu.show(x, y, handle()); 622 623 if(cmd == ACTION_RESTORE) 624 resetColumns(); 625 else if(cmd >> 8 == 1) { 626 const int col = cmd & 0xff; 627 resizeColumn(col, columnWidth(col) ? 0 : m_cols[col].width); 628 } 629 } 630 631 void ListView::resetColumns() 632 { 633 std::vector<int> order(columnCount()); 634 635 for(int i = 0; i < columnCount(); i++) { 636 order[i] = i; 637 638 const Column &col = m_cols[i]; 639 resizeColumn(i, col.test(CollapseFlag) ? 0 : col.width); 640 } 641 642 ListView_SetColumnOrderArray(handle(), columnCount(), order.data()); 643 644 if(m_sort) { 645 setSortArrow(false); 646 m_sort = m_defaultSort; 647 setSortArrow(true); 648 649 m_dirty |= NeedSortFlag; 650 endEdit(); 651 } 652 } 653 654 void ListView::restoreState(Serializer::Data &data) 655 { 656 m_customizable = true; 657 setExStyle(LVS_EX_HEADERDRAGDROP); // enable column reordering 658 659 if(data.empty()) 660 return; 661 662 int col = -1; 663 std::vector<int> order(columnCount()); 664 665 while(col < columnCount() && !data.empty()) { 666 const auto &[left, right] = data.front(); 667 668 switch(col) { 669 case -1: // sort 670 if(left >= 0 && left < columnCount()) 671 sortByColumn(left, right == 0 ? AscendingOrder : DescendingOrder, true); 672 break; 673 default: // column 674 order[col] = left; 675 // raw size should not go through adjustSize (via resizeColumn) 676 ListView_SetColumnWidth(handle(), col, right); 677 break; 678 } 679 680 data.pop_front(); // deletes rec 681 ++col; 682 } 683 684 // fill default values for any columns whose state wasn't saved 685 // (col can't be -1 at this point, the loop above is always run at least once) 686 while(col < columnCount()) { 687 order[col] = col; 688 ++col; 689 } 690 691 ListView_SetColumnOrderArray(handle(), columnCount(), order.data()); 692 } 693 694 void ListView::saveState(Serializer::Data &data) const 695 { 696 const Sort &sort = m_sort.value_or(Sort{}); 697 std::vector<int> order(columnCount()); 698 ListView_GetColumnOrderArray(handle(), columnCount(), order.data()); 699 700 data.push_back({sort.column, sort.order}); 701 702 for(int i = 0; i < columnCount(); i++) 703 data.push_back({order[i], columnWidth(i)}); 704 } 705 706 int ListView::Column::compare(const ListView::Cell &cl, const ListView::Cell &cr) const 707 { 708 if(dataType) { 709 if(!cl.userData) 710 return -1; 711 else if(!cr.userData) 712 return 1; 713 } 714 715 switch(dataType) { 716 case UserType: { // arbitrary data or no data: sort by visible text 717 std::string l = cl.value, r = cr.value; 718 719 boost::algorithm::to_lower(l); 720 boost::algorithm::to_lower(r); 721 722 return l.compare(r); 723 } 724 case VersionType: 725 return static_cast<const VersionName *>(cl.userData)->compare( 726 *static_cast<const VersionName *>(cr.userData)); 727 case TimeType: 728 return static_cast<const Time *>(cl.userData)->compare( 729 *static_cast<const Time *>(cr.userData)); 730 } 731 732 return 0; // to make MSVC happy 733 } 734 735 ListView::Row::Row(void *data, ListView *list) 736 : userData(data), viewIndex(list->rowCount()), userIndex(viewIndex), 737 m_list(list), m_cells(new Cell[m_list->columnCount()]) 738 { 739 } 740 741 void ListView::Row::setCell(const int i, const std::string &val, void *data) 742 { 743 Cell &cell = m_cells[i]; 744 cell.value = val; 745 cell.userData = data; 746 747 m_list->updateCell(userIndex, i); 748 } 749 750 void ListView::Row::setChecked(bool checked) 751 { 752 m_list->setRowIcon(userIndex, checked); 753 } 754 755 std::vector<std::string> ListView::Row::filterValues() const 756 { 757 std::vector<std::string> values; 758 759 for(int ci = 0; ci < m_list->columnCount(); ++ci) { 760 if(m_list->column(ci).test(FilterFlag)) 761 values.push_back(m_cells[ci].value); 762 } 763 764 return values; 765 }