browser.cpp (23325B)
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 "browser.hpp" 19 20 #include "about.hpp" 21 #include "browser_entry.hpp" 22 #include "config.hpp" 23 #include "errors.hpp" 24 #include "index.hpp" 25 #include "listview.hpp" 26 #include "menu.hpp" 27 #include "reapack.hpp" 28 #include "resource.hpp" 29 #include "transaction.hpp" 30 #include "win32.hpp" 31 32 #include <algorithm> 33 34 enum Timers { TIMER_FILTER = 1, TIMER_ABOUT }; 35 36 Browser::Browser() 37 : Dialog(IDD_BROWSER_DIALOG), m_loadState(Init), m_currentIndex(-1) 38 { 39 } 40 41 void Browser::onInit() 42 { 43 m_applyBtn = getControl(IDAPPLY); 44 m_filter = getControl(IDC_FILTER); 45 m_view = getControl(IDC_TABS); 46 m_displayBtn = getControl(IDC_DISPLAY); 47 m_actionsBtn = getControl(IDC_ACTION); 48 49 disable(m_applyBtn); 50 disable(m_actionsBtn); 51 52 // don't forget to update order of enum View in header file 53 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("All")); 54 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("Queued")); 55 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("Installed")); 56 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("Out of date")); 57 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("Obsolete")); 58 SendMessage(m_view, CB_ADDSTRING, 0, (LPARAM)L("Uninstalled")); 59 SendMessage(m_view, CB_SETCURSEL, 0, 0); 60 61 m_list = createControl<ListView>(IDC_LIST, ListView::Columns{ 62 {"Status", 23, ListView::NoLabelFlag}, 63 {"Package", 345, ListView::FilterFlag}, 64 {"Category", 105, ListView::FilterFlag}, 65 {"Version", 55, 0, ListView::VersionType}, 66 {"Author", 95, ListView::FilterFlag}, 67 {"Type", 70}, 68 {"Repository", 120, ListView::CollapseFlag | ListView::FilterFlag}, 69 {"Last Update", 105, 0, ListView::TimeType}, 70 }); 71 72 m_list->onActivate >> [=] { aboutPackage(m_list->itemUnderMouse()); }; 73 m_list->onSelect >> std::bind(&Browser::onSelection, this); 74 m_list->onFillContextMenu >> std::bind(&Browser::fillContextMenu, this, 75 std::placeholders::_1, std::placeholders::_2); 76 m_list->sortByColumn(1); 77 78 Dialog::onInit(); 79 setMinimumSize({600, 250}); 80 81 setAnchor(m_filter, AnchorRight); 82 setAnchor(getControl(IDC_CLEAR), AnchorLeftRight); 83 setAnchor(getControl(IDC_LABEL2), AnchorLeftRight); 84 setAnchor(m_view, AnchorLeftRight); 85 setAnchor(m_displayBtn, AnchorLeftRight); 86 setAnchor(m_list->handle(), AnchorRight | AnchorBottom); 87 setAnchor(getControl(IDC_SELECT), AnchorTop | AnchorBottom); 88 setAnchor(getControl(IDC_UNSELECT), AnchorTop | AnchorBottom); 89 setAnchor(m_actionsBtn, AnchorTop | AnchorBottom); 90 setAnchor(getControl(IDOK), AnchorAll); 91 setAnchor(getControl(IDCANCEL), AnchorAll); 92 setAnchor(m_applyBtn, AnchorAll); 93 94 auto data = m_serializer.read(g_reapack->config()->windowState.browser, 1); 95 restoreState(data); 96 m_list->restoreState(data); 97 98 updateDisplayLabel(); 99 } 100 101 void Browser::onClose() 102 { 103 Serializer::Data data; 104 saveState(data); 105 m_list->saveState(data); 106 g_reapack->config()->windowState.browser = m_serializer.write(data); 107 } 108 109 void Browser::onCommand(const int id, const int event) 110 { 111 using namespace std::placeholders; 112 113 switch(id) { 114 case IDC_TABS: 115 if(event == CBN_SELCHANGE) 116 fillList(); 117 break; 118 case IDC_DISPLAY: 119 displayButton(); 120 break; 121 case IDC_FILTER: 122 if(event == EN_CHANGE) 123 startTimer(200, TIMER_FILTER, false); 124 break; 125 case IDC_CLEAR: 126 setFilter({}); 127 break; 128 case IDC_SELECT: 129 m_list->selectAll(); 130 SetFocus(m_list->handle()); 131 break; 132 case IDC_UNSELECT: 133 m_list->unselectAll(); 134 SetFocus(m_list->handle()); 135 break; 136 case IDC_ACTION: 137 actionsButton(); 138 break; 139 case ACTION_LATEST: 140 currentDo(std::bind(&Browser::installLatest, this, _1, true)); 141 break; 142 case ACTION_LATEST_ALL: 143 installLatestAll(); 144 break; 145 case ACTION_REINSTALL: 146 currentDo(std::bind(&Browser::reinstall, this, _1, true)); 147 break; 148 case ACTION_REINSTALL_ALL: 149 selectionDo(std::bind(&Browser::reinstall, this, _1, false)); 150 break; 151 case ACTION_UNINSTALL: 152 currentDo(std::bind(&Browser::uninstall, this, _1, true)); 153 break; 154 case ACTION_UNINSTALL_ALL: 155 selectionDo(std::bind(&Browser::uninstall, this, _1, false)); 156 break; 157 case ACTION_PIN: 158 currentDo(std::bind(&Browser::toggleFlag, this, 159 _1, Registry::Entry::PinnedFlag)); 160 break; 161 case ACTION_BLEEDINGEDGE: 162 currentDo(std::bind(&Browser::toggleFlag, this, 163 _1, Registry::Entry::BleedingEdgeFlag)); 164 break; 165 case ACTION_ABOUT_PKG: 166 aboutPackage(m_currentIndex); 167 break; 168 case ACTION_ABOUT_REMOTE: 169 aboutRemote(m_currentIndex); 170 break; 171 case ACTION_RESET_ALL: 172 selectionDo(std::bind(&Browser::resetActions, this, _1)); 173 break; 174 case ACTION_COPY: 175 copy(); 176 break; 177 case ACTION_SYNCHRONIZE: 178 g_reapack->synchronizeAll(); 179 break; 180 case ACTION_REFRESH: 181 refresh(true); 182 break; 183 case ACTION_UPLOAD: 184 g_reapack->uploadPackage(); 185 break; 186 case ACTION_MANAGE: 187 g_reapack->manageRemotes(); 188 break; 189 case ACTION_FILTERTYPE: 190 m_typeFilter = std::nullopt; 191 fillList(); 192 break; 193 case IDOK: 194 case IDAPPLY: 195 if(confirm()) { 196 if(!apply() || id == IDAPPLY) 197 break; 198 } 199 else 200 break; 201 [[fallthrough]]; 202 case IDCANCEL: 203 if(m_loadState == Loading) 204 hide(); // keep ourselves alive 205 else 206 close(); 207 break; 208 default: 209 if(id >> 8 == ACTION_VERSION) 210 currentDo(std::bind(&Browser::installVersion, this, _1, id & 0xff)); 211 else if(id >> 8 == ACTION_FILTERTYPE) { 212 m_typeFilter = static_cast<Package::Type>(id & 0xff); 213 fillList(); 214 } 215 break; 216 } 217 } 218 219 bool Browser::onKeyDown(const int key, const int mods) 220 { 221 if(GetFocus() != m_list->handle()) { 222 if(mods == 0 && (key == VK_UP || key == VK_DOWN)) 223 SetFocus(m_list->handle()); 224 225 return false; 226 } 227 228 if(mods == CtrlModifier && key == 'A') 229 m_list->selectAll(); 230 else if(mods == (CtrlModifier | ShiftModifier) && key == 'A') 231 m_list->unselectAll(); 232 else if(mods == CtrlModifier && key == 'C') 233 copy(); 234 else if(!mods && key == VK_F5) 235 refresh(true); 236 else if(!mods && key == VK_SPACE) 237 aboutPackage(m_list->currentIndex()); 238 else 239 return false; 240 241 return true; 242 } 243 244 void Browser::onTimer(const int id) 245 { 246 switch(id) { 247 case TIMER_FILTER: 248 updateFilter(); 249 break; 250 case TIMER_ABOUT: 251 updateAbout(); 252 break; 253 } 254 } 255 256 void Browser::onSelection() 257 { 258 setEnabled(m_list->hasSelection(), m_actionsBtn); 259 startTimer(100, TIMER_ABOUT); 260 } 261 262 bool Browser::fillContextMenu(Menu &menu, const int index) 263 { 264 m_currentIndex = index; 265 fillMenu(menu); 266 267 menu.addAction("&Select all", IDC_SELECT); 268 menu.addAction("&Unselect all", IDC_UNSELECT); 269 270 return true; 271 } 272 273 void Browser::fillMenu(Menu &menu) 274 { 275 const Entry *entry = getEntry(m_currentIndex); 276 277 if(m_list->selectionSize() > 1) { 278 fillSelectionMenu(menu); 279 280 if(entry) { 281 menu.addSeparator(); 282 Menu pkgMenu = menu.addMenu("Package under cursor"); 283 entry->fillMenu(pkgMenu); 284 } 285 } 286 else if(entry) 287 entry->fillMenu(menu); 288 289 if(!menu.empty()) 290 menu.addSeparator(); 291 292 if(m_list->hasSelection()) 293 menu.addAction("&Copy package name", ACTION_COPY); 294 } 295 296 void Browser::fillSelectionMenu(Menu &menu) 297 { 298 int selFlags = 0; 299 300 for(const int index : m_list->selection()) 301 selFlags |= getEntry(index)->possibleActions(false); 302 303 menu.setEnabled(selFlags & Entry::CanInstallLatest, 304 menu.addAction("&Install/update selection", ACTION_LATEST_ALL)); 305 menu.setEnabled(selFlags & Entry::CanReinstall, 306 menu.addAction("&Reinstall selection", ACTION_REINSTALL_ALL)); 307 menu.setEnabled(selFlags & Entry::CanUninstall, 308 menu.addAction("&Uninstall selection", ACTION_UNINSTALL_ALL)); 309 menu.setEnabled(selFlags & Entry::CanClearQueued, 310 menu.addAction("&Clear queued actions", ACTION_RESET_ALL)); 311 } 312 313 void Browser::updateDisplayLabel() 314 { 315 Win32::setWindowText(m_displayBtn, String::format("%s/%s package%s...", 316 String::number(m_list->visibleRowCount()).c_str(), 317 String::number(m_entries.size()).c_str(), m_entries.size() == 1 ? "" : "s" 318 ).c_str()); 319 } 320 321 void Browser::displayButton() 322 { 323 constexpr std::pair<const char *, Package::Type> types[] { 324 {"&Automation Items", Package::AutomationItemType}, 325 {"&Effects", Package::EffectType}, 326 {"E&xtensions", Package::ExtensionType}, 327 {"&Language Packs", Package::LangPackType}, 328 {"&MIDI Note Names", Package::MIDINoteNamesType}, 329 {"&Project Templates", Package::ProjectTemplateType}, 330 {"&Scripts", Package::ScriptType}, 331 {"&Themes", Package::ThemeType}, 332 {"&Track Templates", Package::TrackTemplateType}, 333 {"&Web Interfaces", Package::WebInterfaceType}, 334 335 {"&Other packages", Package::UnknownType}, 336 }; 337 338 Menu menu; 339 340 auto index = menu.addAction("&All packages", ACTION_FILTERTYPE); 341 if(!m_typeFilter) 342 menu.checkRadio(index); 343 344 for(const auto &[name, type] : types) { 345 auto index = menu.addAction(name, type | (ACTION_FILTERTYPE << 8)); 346 347 if(m_typeFilter && m_typeFilter == type) 348 menu.checkRadio(index); 349 } 350 351 menu.addSeparator(); 352 353 menu.addAction("&Synchronize packages", ACTION_SYNCHRONIZE); 354 menu.addAction("&Refresh repositories", ACTION_REFRESH); 355 menu.addAction("Package &editor", ACTION_UPLOAD); 356 menu.addAction("&Manage repositories...", ACTION_MANAGE); 357 358 menu.show(m_displayBtn, handle()); 359 } 360 361 void Browser::actionsButton() 362 { 363 m_currentIndex = m_list->currentIndex(); 364 365 Menu menu; 366 fillMenu(menu); 367 menu.show(m_actionsBtn, handle()); 368 } 369 370 bool Browser::isFiltered(Package::Type type) const 371 { 372 if(!m_typeFilter) 373 return false; 374 375 switch(type) { 376 case Package::UnknownType: 377 case Package::ScriptType: 378 case Package::EffectType: 379 case Package::ExtensionType: 380 case Package::ThemeType: 381 case Package::LangPackType: 382 case Package::WebInterfaceType: 383 case Package::ProjectTemplateType: 384 case Package::TrackTemplateType: 385 case Package::MIDINoteNamesType: 386 case Package::AutomationItemType: 387 break; 388 case Package::DataType: 389 type = Package::UnknownType; 390 break; 391 } 392 393 return m_typeFilter != type; 394 } 395 396 void Browser::updateFilter() 397 { 398 stopTimer(TIMER_FILTER); 399 400 { 401 ListView::BeginEdit edit(m_list); 402 m_list->setFilter(Win32::getWindowText(m_filter)); 403 } 404 updateDisplayLabel(); 405 } 406 407 void Browser::updateAbout() 408 { 409 stopTimer(TIMER_ABOUT); 410 411 About *about = g_reapack->about(false); 412 413 if(!about) 414 return; 415 416 const auto index = m_list->currentIndex(); 417 418 if(about->testDelegate<AboutIndexDelegate>()) 419 aboutRemote(index, false); 420 else if(about->testDelegate<AboutPackageDelegate>()) 421 aboutPackage(index, false); 422 } 423 424 void Browser::refresh(const bool stale) 425 { 426 switch(m_loadState) { 427 case DeferredLoaded: 428 // We already processed this transaction immediately before. 429 m_loadState = Loaded; 430 return; 431 case Loading: 432 // Don't refresh again while currently refreshing. 433 return; 434 default: 435 break; 436 } 437 438 const std::vector<Remote> &remotes = g_reapack->config()->remotes.getEnabled(); 439 440 if(remotes.empty()) { 441 if(!isVisible() || stale) { 442 show(); 443 444 Win32::messageBox(handle(), "No repository enabled!\n" 445 "Enable or import repositories from Extensions > ReaPack > Manage repositories.", 446 "Browse packages", MB_OK); 447 } 448 449 // Clear the list if it were previously filled. 450 populate({}, nullptr); 451 return; 452 } 453 454 if(Transaction *tx = g_reapack->setupTransaction()) { 455 const bool isFirstLoad = m_loadState == Init; 456 m_loadState = Loading; 457 458 tx->fetchIndexes(remotes, stale); 459 tx->onFinish >> [=] { 460 if(isFirstLoad || isVisible()) { 461 populate(tx->getIndexes(remotes), tx->registry()); 462 463 // Ignore the next call to refreshBrowser() if we know we'll be 464 // requested to handle the very same transaction. 465 m_loadState = tx->receipt()->test(Receipt::RefreshBrowser) ? 466 DeferredLoaded : Loaded; 467 } 468 else { 469 // User manually asked to refresh the browser but closed the window 470 // before it could finished fetching the up to date indexes. 471 close(); 472 } 473 }; 474 475 tx->runTasks(); 476 } 477 } 478 479 void Browser::populate(const std::vector<IndexPtr> &indexes, const Registry *reg) 480 { 481 // keep previous entries in memory a bit longer for #transferActions 482 std::vector<Entry> oldEntries; 483 std::swap(m_entries, oldEntries); 484 485 m_currentIndex = -1; 486 487 for(const IndexPtr &index : indexes) { 488 for(const Package *pkg : index->packages()) 489 m_entries.push_back({pkg, reg->getEntry(pkg), index}); 490 491 // obsolete packages 492 for(const Registry::Entry ®Entry : reg->getEntries(index->name())) { 493 if(!index->find(regEntry.category, regEntry.package)) 494 m_entries.push_back({regEntry, index}); 495 } 496 } 497 498 transferActions(); 499 fillList(); 500 501 if(!isVisible()) { 502 show(); 503 // required on REAPER v6/macOS (for some reason the progress dialog 504 // stops the first control from being focused) 505 SetFocus(m_filter); 506 } 507 } 508 509 void Browser::setFilter(const std::string &newFilter) 510 { 511 Win32::setWindowText(m_filter, newFilter.c_str()); 512 updateFilter(); // don't wait for the timer, update now! 513 SetFocus(m_filter); 514 } 515 516 void Browser::transferActions() 517 { 518 std::list<Entry *> oldActions; 519 std::swap(m_actions, oldActions); 520 521 for(Entry *oldEntry : oldActions) { 522 const auto &entryIt = find(m_entries.begin(), m_entries.end(), *oldEntry); 523 if(entryIt == m_entries.end()) 524 continue; 525 526 if(oldEntry->target) { 527 const Version *target = *oldEntry->target; 528 529 if(target) { 530 const Package *pkg = entryIt->package; 531 if(!pkg || !(target = pkg->findVersion(target->name()))) 532 continue; 533 } 534 535 entryIt->target = target; 536 } 537 538 if(oldEntry->flags) 539 entryIt->flags = *oldEntry->flags; 540 541 m_actions.push_back(&*entryIt); 542 } 543 544 if(m_actions.empty()) 545 disable(m_applyBtn); 546 } 547 548 void Browser::fillList() 549 { 550 ListView::BeginEdit edit(m_list); 551 552 const int scroll = m_list->scroll(); 553 554 std::vector<int> selectIndexes = m_list->selection(); 555 std::vector<const Entry *> oldSelection(selectIndexes.size()); 556 for(size_t i = 0; i < selectIndexes.size(); i++) 557 oldSelection[i] = static_cast<Entry *>(m_list->row(selectIndexes[i])->userData); 558 selectIndexes.clear(); // will put new indexes below 559 560 m_list->clear(); 561 m_list->reserveRows(m_entries.size()); 562 m_list->setFilter(Win32::getWindowText(m_filter)); // in case filter settings changed 563 564 for(const Entry &entry : m_entries) { 565 if(!match(entry)) 566 continue; 567 568 auto row = m_list->createRow((void *)&entry); 569 entry.updateRow(row); 570 571 const auto &matchingEntryIt = find_if(oldSelection.begin(), oldSelection.end(), 572 [&entry] (const Entry *oldEntry) { return *oldEntry == entry; }); 573 574 if(matchingEntryIt != oldSelection.end()) 575 selectIndexes.push_back(row->index()); 576 } 577 578 m_list->setScroll(scroll); 579 580 // restore selection only after having sorted the table 581 // in order to get the same scroll position as before if possible 582 for(const int index : selectIndexes) 583 m_list->select(index); 584 585 m_list->endEdit(); // filter before calling updateDisplayLabel 586 updateDisplayLabel(); 587 } 588 589 bool Browser::match(const Entry &entry) const 590 { 591 if(isFiltered(entry.type())) 592 return false; 593 594 switch(currentView()) { 595 case AllView: 596 break; 597 case QueuedView: 598 if(!hasAction(&entry)) 599 return false; 600 break; 601 case InstalledView: 602 if(!entry.test(Entry::InstalledFlag)) 603 return false; 604 break; 605 case OutOfDateView: 606 if(!entry.test(Entry::OutOfDateFlag)) 607 return false; 608 break; 609 case UninstalledView: 610 if(!entry.test(Entry::UninstalledFlag)) 611 return false; 612 break; 613 case ObsoleteView: 614 if(!entry.test(Entry::ObsoleteFlag)) 615 return false; 616 break; 617 } 618 619 return true; 620 } 621 622 auto Browser::getEntry(const int index) -> Entry * 623 { 624 if(index < 0) 625 return nullptr; 626 else 627 return static_cast<Entry *>(m_list->row(index)->userData); 628 } 629 630 void Browser::aboutPackage(const int index, const bool focus) 631 { 632 const Entry *entry = getEntry(index); 633 634 if(entry && entry->package) { 635 g_reapack->about()->setDelegate(std::make_shared<AboutPackageDelegate>( 636 entry->package, entry->regEntry.version), focus); 637 } 638 } 639 640 void Browser::aboutRemote(const int index, const bool focus) 641 { 642 if(const Entry *entry = getEntry(index)) { 643 g_reapack->about()->setDelegate( 644 std::make_shared<AboutIndexDelegate>(entry->index), focus); 645 } 646 } 647 648 void Browser::installLatestAll() 649 { 650 InstallOpts &installOpts = g_reapack->config()->install; 651 const bool isEverything = static_cast<size_t>(m_list->selectionSize()) == m_entries.size(); 652 653 if(isEverything && !installOpts.autoInstall) { 654 const int btn = Win32::messageBox(handle(), "Do you want ReaPack to install" 655 " new packages automatically when synchronizing in the future?\n\nThis" 656 " setting can also be customized globally or on a per-repository basis" 657 " in ReaPack > Manage repositories.", 658 "Install every available packages", MB_YESNOCANCEL); 659 660 switch(btn) { 661 case IDYES: 662 installOpts.autoInstall = true; 663 break; 664 case IDCANCEL: 665 return; 666 } 667 } 668 669 selectionDo(std::bind(&Browser::installLatest, this, std::placeholders::_1, false)); 670 } 671 672 void Browser::installLatest(const int index, const bool toggle) 673 { 674 const Entry *entry = getEntry(index); 675 676 if(entry && entry->test(Entry::CanInstallLatest, toggle)) 677 toggleTarget(index, entry->latest); 678 } 679 680 void Browser::reinstall(const int index, const bool toggle) 681 { 682 const Entry *entry = getEntry(index); 683 684 if(entry && entry->test(Entry::CanReinstall, toggle)) 685 toggleTarget(index, entry->current); 686 } 687 688 void Browser::installVersion(const int index, const size_t verIndex) 689 { 690 const Entry *entry = getEntry(index); 691 if(!entry) 692 return; 693 694 const auto &versions = entry->package->versions(); 695 696 if(verIndex >= versions.size()) 697 return; 698 699 const Version *target = entry->package->version(verIndex); 700 701 if(target == entry->current) 702 resetTarget(index); 703 else 704 toggleTarget(index, target); 705 } 706 707 void Browser::uninstall(const int index, const bool toggle) 708 { 709 const Entry *entry = getEntry(index); 710 711 if(entry && entry->test(Entry::CanUninstall, toggle)) 712 toggleTarget(index, nullptr); 713 } 714 715 void Browser::toggleFlag(const int index, const int mask) 716 { 717 Entry *entry = getEntry(index); 718 if(!entry || !entry->test(Entry::CanToggleFlags)) 719 return; 720 721 const int newVal = mask ^ entry->flags.value_or(entry->regEntry.flags); 722 723 if(newVal == entry->regEntry.flags) 724 entry->flags = std::nullopt; 725 else 726 entry->flags = newVal; 727 728 updateAction(index); 729 } 730 731 bool Browser::hasAction(const Entry *entry) const 732 { 733 return count(m_actions.begin(), m_actions.end(), entry) > 0; 734 } 735 736 void Browser::toggleTarget(const int index, const Version *target) 737 { 738 Entry *entry = getEntry(index); 739 740 if(entry->target && *entry->target == target) 741 entry->target = std::nullopt; 742 else 743 entry->target = target; 744 745 updateAction(index); 746 } 747 748 void Browser::resetTarget(const int index) 749 { 750 Entry *entry = getEntry(index); 751 752 if(entry->target) { 753 entry->target = std::nullopt; 754 updateAction(index); 755 } 756 } 757 758 void Browser::resetActions(const int index) 759 { 760 Entry *entry = getEntry(index); 761 762 if(entry->target) 763 entry->target = std::nullopt; 764 if(entry->flags) 765 entry->flags = std::nullopt; 766 767 updateAction(index); 768 } 769 770 void Browser::updateAction(const int index) 771 { 772 Entry *entry = getEntry(index); 773 if(!entry) 774 return; 775 776 const auto &it = find(m_actions.begin(), m_actions.end(), entry); 777 if(!entry->target && (!entry->flags || !entry->test(Entry::CanToggleFlags))) { 778 if(it != m_actions.end()) 779 m_actions.erase(it); 780 } 781 else if(it == m_actions.end()) 782 m_actions.push_back(entry); 783 784 if(currentView() == QueuedView && !hasAction(entry)) 785 m_list->removeRow(index); 786 else 787 m_list->row(index)->setCell(0, entry->displayState()); 788 } 789 790 void Browser::listDo(const std::function<void (int)> &func, const std::vector<int> &indexes) 791 { 792 ListView::BeginEdit edit(m_list); 793 794 int lastSize = m_list->rowCount(); 795 int offset = 0; 796 797 // Assumes the index vector is sorted 798 for(const int index : indexes) { 799 func(index - offset); 800 801 // handle row removal 802 int newSize = m_list->rowCount(); 803 if(newSize < lastSize) 804 offset++; 805 lastSize = newSize; 806 } 807 808 if(offset) // rows were deleted 809 updateDisplayLabel(); 810 811 if(m_actions.empty()) 812 disable(m_applyBtn); 813 else 814 enable(m_applyBtn); 815 } 816 817 void Browser::currentDo(const std::function<void (int)> &func) 818 { 819 listDo(func, {m_currentIndex}); 820 } 821 822 void Browser::selectionDo(const std::function<void (int)> &func) 823 { 824 listDo(func, m_list->selection()); 825 } 826 827 auto Browser::currentView() const -> View 828 { 829 return static_cast<View>(SendMessage(m_view, CB_GETCURSEL, 0, 0)); 830 } 831 832 void Browser::copy() 833 { 834 std::vector<std::string> values; 835 836 for(const int index : m_list->selection(false)) 837 values.push_back(getEntry(index)->displayName()); 838 839 setClipboard(values); 840 } 841 842 bool Browser::confirm() const 843 { 844 // count uninstallation actions 845 const size_t count = count_if(m_actions.begin(), m_actions.end(), 846 [](const Entry *e) { return e->target && *e->target == nullptr; }); 847 848 if(!count) 849 return true; 850 851 return IDYES == Win32::messageBox(handle(), String::format( 852 "Are you sure to uninstall %zu package%s?\nThe files and settings will" 853 " be permanently deleted from this computer.", 854 count, count == 1 ? "" : "s" 855 ).c_str(), "ReaPack Query", MB_YESNO); 856 } 857 858 bool Browser::apply() 859 { 860 if(m_actions.empty()) 861 return true; 862 863 Transaction *tx = g_reapack->setupTransaction(); 864 865 if(!tx) 866 return false; 867 868 for(Entry *entry : m_actions) { 869 if(entry->target) { 870 const Version *target = *entry->target; 871 872 if(target) 873 tx->install(target, entry->flags.value_or(entry->regEntry.flags)); 874 else 875 tx->uninstall(entry->regEntry); 876 877 entry->target = std::nullopt; 878 } 879 else if(entry->flags) { 880 tx->setFlags(entry->regEntry, *entry->flags); 881 entry->flags = std::nullopt; 882 } 883 } 884 885 m_actions.clear(); 886 disable(m_applyBtn); 887 888 if(!tx->runTasks()) { 889 // This is an asynchronous transaction. 890 // Updating the state column of all rows (not just visible ones since the 891 // hidden rows can be filtered into view again by user at any time) right away 892 // to give visual feedback. 893 ListView::BeginEdit edit(m_list); 894 895 if(currentView() == QueuedView) 896 m_list->clear(); 897 else { 898 for(int i = 0, count = m_list->rowCount(); i < count; ++i) 899 m_list->row(i)->setCell(0, getEntry(i)->displayState()); 900 } 901 } 902 903 return true; 904 }