reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

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 &regEntry : 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 }