reapack

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

about.cpp (17740B)


      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 "about.hpp"
     19 
     20 #include "browser.hpp"
     21 #include "buildinfo.hpp"
     22 #include "config.hpp"
     23 #include "errors.hpp"
     24 #include "filesystem.hpp"
     25 #include "index.hpp"
     26 #include "listview.hpp"
     27 #include "menu.hpp"
     28 #include "reapack.hpp"
     29 #include "registry.hpp"
     30 #include "remote.hpp"
     31 #include "report.hpp"
     32 #include "resource.hpp"
     33 #include "richedit.hpp"
     34 #include "tabbar.hpp"
     35 #include "transaction.hpp"
     36 #include "win32.hpp"
     37 
     38 #include <boost/algorithm/string.hpp>
     39 #include <iomanip>
     40 #include <sstream>
     41 
     42 enum {
     43   ACTION_ABOUT_PKG = 300, ACTION_FIND_IN_BROWSER,
     44   ACTION_COPY_URL, ACTION_LOCATE
     45 };
     46 
     47 About::About()
     48   : Dialog(IDD_ABOUT_DIALOG)
     49 {
     50 }
     51 
     52 void About::onInit()
     53 {
     54   Dialog::onInit();
     55 
     56   m_tabs = createControl<TabBar>(IDC_TABS, this);
     57   m_desc = createControl<RichEdit>(IDC_ABOUT);
     58 
     59   m_menu = createControl<ListView>(IDC_MENU);
     60   m_menu->onSelect >> std::bind(&About::updateList, this);
     61 
     62   m_list = createControl<ListView>(IDC_LIST);
     63   m_list->onFillContextMenu >> [=] (Menu &m, int i) { return m_delegate->fillContextMenu(m, i); };
     64   m_list->onActivate >> [=] { m_delegate->itemActivated(); };
     65 
     66   setMinimumSize({560, 300});
     67   setAnchor(m_tabs->handle(), AnchorRight | AnchorBottom);
     68   setAnchor(m_desc->handle(), AnchorRight | AnchorBottom);
     69   setAnchor(m_menu->handle(), AnchorBottom);
     70   setAnchor(m_list->handle(), AnchorRight | AnchorBottom);
     71   setAnchor(getControl(IDC_REPORT), AnchorRight | AnchorBottom);
     72   setAnchor(getControl(IDC_CHANGELOG), AnchorRight | AnchorBottom);
     73   setAnchor(getControl(IDC_WEBSITE), AnchorTop | AnchorBottom);
     74   setAnchor(getControl(IDC_DONATE), AnchorTop | AnchorBottom);
     75   setAnchor(getControl(IDC_SCREENSHOT), AnchorTop | AnchorBottom);
     76   setAnchor(getControl(IDC_ACTION), AnchorAll);
     77   setAnchor(getControl(IDOK), AnchorAll);
     78 
     79   auto data = m_serializer.read(g_reapack->config()->windowState.about, 1);
     80   restoreState(data);
     81 }
     82 
     83 void About::onClose()
     84 {
     85   Serializer::Data data;
     86   saveState(data);
     87   g_reapack->config()->windowState.about = m_serializer.write(data);
     88 }
     89 
     90 void About::onCommand(const int id, int)
     91 {
     92   switch(id) {
     93   case IDOK:
     94   case IDCANCEL:
     95     close();
     96     break;
     97   default:
     98     if(m_links.count(id))
     99       selectLink(id);
    100     else if(m_delegate)
    101       m_delegate->onCommand(id);
    102     break;
    103   }
    104 }
    105 
    106 bool About::onKeyDown(const int key, const int mods)
    107 {
    108   if(GetFocus() != m_list->handle())
    109     return false;
    110 
    111   if(mods == CtrlModifier && key == 'C')
    112     m_delegate->itemCopy();
    113   else
    114     return false;
    115 
    116   return true;
    117 }
    118 
    119 void About::setDelegate(const DelegatePtr &delegate, const bool focus)
    120 {
    121   if(m_delegate && delegate->data() == m_delegate->data()) {
    122     if(focus)
    123       setFocus(); // also calls show()
    124     return;
    125   }
    126 
    127   // prevent fast flickering on Windows
    128   InhibitControl block(handle());
    129 
    130   m_tabs->clear();
    131   m_menu->reset();
    132   m_menu->sortByColumn(0);
    133   m_list->reset();
    134   m_list->sortByColumn(0);
    135 
    136   m_delegate = nullptr;
    137   m_links.clear();
    138 
    139   constexpr int controls[] {
    140     IDC_ABOUT,
    141     IDC_MENU,
    142     IDC_LIST,
    143     IDC_CHANGELOG,
    144     IDC_REPORT,
    145     IDC_WEBSITE,
    146     IDC_SCREENSHOT,
    147     IDC_DONATE,
    148     IDC_ACTION,
    149   };
    150 
    151   for(const int control : controls)
    152     hide(getControl(control));
    153 
    154   ListView::BeginEdit menuEdit(m_menu);
    155   m_delegate = delegate;
    156   m_delegate->init(this);
    157 
    158   m_currentIndex = -255;
    159   updateList();
    160 
    161   m_menu->autoSizeHeader();
    162   m_list->autoSizeHeader();
    163 
    164   if(focus) {
    165     show();
    166     m_tabs->setFocus();
    167   }
    168 }
    169 
    170 void About::setTitle(const std::string &what)
    171 {
    172   Win32::setWindowText(handle(), what.c_str());
    173 }
    174 
    175 void About::setMetadata(const Metadata *metadata, const bool substitution)
    176 {
    177   std::string aboutText(metadata->about());
    178 
    179   if(substitution) {
    180     constexpr std::pair<const char *, const char *> replacements[] {
    181       { "[[REAPACK_VERSION]]",   REAPACK_VERSION   },
    182       { "[[REAPACK_REVISION]]",  REAPACK_REVISION  },
    183       { "[[REAPACK_BUILDTIME]]", REAPACK_BUILDTIME },
    184     };
    185 
    186     for(const auto &replacement : replacements)
    187       boost::replace_all(aboutText, replacement.first, replacement.second);
    188   }
    189 
    190   if(aboutText.empty())
    191     m_desc->setPlainText("This package or repository does not provide any documentation.");
    192   else if(!m_desc->setRichText(aboutText))
    193     m_desc->setPlainText("Could not load RTF document.");
    194 
    195   m_tabs->addTab({"About", {m_desc->handle()}});
    196 
    197   const auto &getLinkControl = [](const Metadata::LinkType type) {
    198     switch(type) {
    199     case Metadata::WebsiteLink:
    200       return IDC_WEBSITE;
    201     case Metadata::DonationLink:
    202       return IDC_DONATE;
    203     case Metadata::ScreenshotLink:
    204       return IDC_SCREENSHOT;
    205     }
    206 
    207     return IDC_WEBSITE; // make MSVC happy
    208   };
    209 
    210   RECT rect;
    211   GetWindowRect(getControl(IDC_WEBSITE), &rect);
    212   ScreenToClient(handle(), (LPPOINT)&rect);
    213   ScreenToClient(handle(), ((LPPOINT)&rect)+1);
    214 
    215   const int shift = (rect.right - rect.left) + 4;
    216 
    217   for(const auto &[type, link] : metadata->links()) {
    218     const int control = getLinkControl(type);
    219 
    220     if(!m_links.count(control)) {
    221       HWND handle = getControl(control);
    222       setAnchorPos(handle, &rect.left, nullptr, &rect.right);
    223       show(handle);
    224 
    225       rect.left += shift;
    226       rect.right += shift;
    227 
    228       m_links[control] = {};
    229     }
    230 
    231     m_links[control].push_back(&link);
    232   }
    233 
    234   onResize(); // update the position of link buttons
    235 }
    236 
    237 void About::setAction(const std::string &label)
    238 {
    239   HWND btn = getControl(IDC_ACTION);
    240   Win32::setWindowText(btn, label.c_str());
    241   show(btn);
    242 }
    243 
    244 void About::selectLink(const int ctrl)
    245 {
    246   const auto &links = m_links[ctrl];
    247   const int count = static_cast<int>(links.size());
    248 
    249   m_tabs->setFocus();
    250 
    251   if(count == 1) {
    252     Win32::shellExecute(links.front()->url.c_str());
    253     return;
    254   }
    255 
    256   Menu menu;
    257 
    258   for(int i = 0; i < count; i++) {
    259     const std::string &name = boost::replace_all_copy(links[i]->name, "&", "&&");
    260     menu.addAction(name.c_str(), i | (ctrl << 8));
    261   }
    262 
    263   const int choice = menu.show(getControl(ctrl), handle());
    264 
    265   if(choice >> 8 == ctrl)
    266     Win32::shellExecute(links[choice & 0xff]->url.c_str());
    267 }
    268 
    269 void About::updateList()
    270 {
    271   const int index = m_menu->currentIndex();
    272 
    273   // do nothing when the selection is cleared, except for the initial execution
    274   if((index < 0 && m_currentIndex != -255) || index == m_currentIndex)
    275     return;
    276 
    277   ListView::BeginEdit edit(m_list);
    278   m_list->clear();
    279 
    280   m_delegate->updateList(index);
    281   m_currentIndex = index;
    282 }
    283 
    284 AboutIndexDelegate::AboutIndexDelegate(const IndexPtr &index)
    285   : m_index(index)
    286 {
    287 }
    288 
    289 void AboutIndexDelegate::init(About *dialog)
    290 {
    291   m_dialog = dialog;
    292 
    293   dialog->setTitle(m_index->name());
    294   dialog->setMetadata(m_index->metadata(), m_index->name() == "ReaPack");
    295   dialog->setAction("Install/update " + m_index->name());
    296 
    297   dialog->tabs()->addTab({"Packages",
    298     {dialog->menu()->handle(), dialog->list()->handle()}});
    299   dialog->tabs()->addTab({"Installed Files",
    300     {dialog->getControl(IDC_REPORT)}});
    301 
    302   dialog->menu()->addColumn({"Category", 142});
    303 
    304   dialog->menu()->reserveRows(m_index->categories().size() + 1);
    305   dialog->menu()->createRow()->setCell(0, "<All Packages>");
    306 
    307   for(const Category *cat : m_index->categories())
    308     dialog->menu()->createRow()->setCell(0, cat->name());
    309 
    310   dialog->list()->addColumn({"Package", 382});
    311   dialog->list()->addColumn({"Version", 80, 0, ListView::VersionType});
    312   dialog->list()->addColumn({"Author", 90});
    313 
    314   initInstalledFiles();
    315 }
    316 
    317 void AboutIndexDelegate::initInstalledFiles()
    318 {
    319   const HWND report = m_dialog->getControl(IDC_REPORT);
    320 
    321   std::set<Registry::File> allFiles;
    322 
    323   try {
    324     Registry reg(Path::REGISTRY.prependRoot());
    325     for(const Registry::Entry &entry : reg.getEntries(m_index->name())) {
    326       const std::vector<Registry::File> &files = reg.getFiles(entry);
    327       allFiles.insert(files.begin(), files.end());
    328     }
    329   }
    330   catch(const reapack_error &e) {
    331     Win32::setWindowText(report, String::format(
    332       "The file list is currently unavailable.\x20"
    333       "Retry later when all installation task are completed.\r\n"
    334       "\r\nError description: %s", e.what()
    335     ).c_str());
    336     return;
    337   }
    338 
    339   if(allFiles.empty()) {
    340     Win32::setWindowText(report,
    341       "This repository does not own any file on your computer at this time.\r\n"
    342 
    343       "It is either not yet installed or it does not provide "
    344       "any package compatible with your system.");
    345   }
    346   else {
    347     std::stringstream stream;
    348 
    349     for(const Registry::File &file : allFiles) {
    350       stream << file.path.join();
    351       if(file.sections) // is this file registered in the action list?
    352         stream << '*';
    353       stream << "\r\n";
    354     }
    355 
    356     Win32::setWindowText(report, stream.str().c_str());
    357   }
    358 }
    359 
    360 void AboutIndexDelegate::updateList(const int index)
    361 {
    362   // -1: all packages, >0 selected category
    363   const int catIndex = index - 1;
    364 
    365   const std::vector<const Package *> *packages;
    366 
    367   if(catIndex < 0)
    368     packages = &m_index->packages();
    369   else
    370     packages = &m_index->category(catIndex)->packages();
    371 
    372   m_dialog->list()->reserveRows(packages->size());
    373 
    374   for(const Package *pkg : *packages) {
    375     int c = 0;
    376     const Version *lastVer = pkg->lastVersion();
    377 
    378     auto row = m_dialog->list()->createRow((void *)pkg);
    379     row->setCell(c++, pkg->displayName());
    380     row->setCell(c++, lastVer->name().toString(), (void *)&lastVer->name());
    381     row->setCell(c++, lastVer->displayAuthor());
    382   }
    383 }
    384 
    385 bool AboutIndexDelegate::fillContextMenu(Menu &menu, const int index) const
    386 {
    387   if(index < 0)
    388     return false;
    389 
    390   menu.addAction("Find in the &browser", ACTION_FIND_IN_BROWSER);
    391   menu.addAction("About this &package", ACTION_ABOUT_PKG);
    392 
    393   return true;
    394 }
    395 
    396 void AboutIndexDelegate::onCommand(const int id)
    397 {
    398   switch(id) {
    399   case ACTION_FIND_IN_BROWSER:
    400     findInBrowser();
    401     break;
    402   case ACTION_ABOUT_PKG:
    403     aboutPackage();
    404     break;
    405   case IDC_ACTION:
    406     install();
    407     break;
    408   }
    409 }
    410 
    411 const Package *AboutIndexDelegate::currentPackage() const
    412 {
    413   const int index = m_dialog->list()->currentIndex();
    414 
    415   if(index < 0)
    416     return nullptr;
    417   else
    418     return static_cast<const Package *>(m_dialog->list()->row(index)->userData);
    419 }
    420 
    421 void AboutIndexDelegate::findInBrowser()
    422 {
    423   Browser *browser = g_reapack->browsePackages();
    424   if(!browser)
    425     return;
    426 
    427   const Package *pkg = currentPackage();
    428 
    429   std::ostringstream stream;
    430   stream << '^' << quoted(pkg->displayName()) << "$ ^" << quoted(m_index->name()) << '$';
    431   browser->setFilter(stream.str());
    432 }
    433 
    434 void AboutIndexDelegate::aboutPackage()
    435 {
    436   const Package *pkg = currentPackage();
    437 
    438   VersionName current;
    439 
    440   try {
    441     Registry reg(Path::REGISTRY.prependRoot());
    442     current = reg.getEntry(pkg).version;
    443   }
    444   catch(const reapack_error &) {}
    445 
    446   m_dialog->setDelegate(std::make_shared<AboutPackageDelegate>(pkg, current));
    447 }
    448 
    449 void AboutIndexDelegate::itemCopy()
    450 {
    451   if(const Package *pkg = currentPackage())
    452     m_dialog->setClipboard(pkg->displayName());
    453 }
    454 
    455 void AboutIndexDelegate::install()
    456 {
    457   enum { INSTALL_ALL = 80, UPDATE_ONLY, OPEN_BROWSER };
    458 
    459   Menu menu;
    460   menu.addAction("Install all packages in this repository", INSTALL_ALL);
    461   menu.addAction("Install individual packages in this repository", OPEN_BROWSER);
    462   menu.addAction("Update installed packages only", UPDATE_ONLY);
    463 
    464   const int choice = menu.show(m_dialog->getControl(IDC_ACTION), m_dialog->handle());
    465 
    466   if(!choice)
    467     return;
    468 
    469   Remote remote = g_reapack->remote(m_index->name());
    470 
    471   if(!remote) {
    472     // In case the user uninstalled the repository while this dialog was opened
    473     Win32::messageBox(m_dialog->handle(),
    474       "This repository cannot be found in your current configuration.",
    475       "ReaPack", MB_OK);
    476     return;
    477   }
    478 
    479   if(choice == OPEN_BROWSER) {
    480     if(Browser *browser = g_reapack->browsePackages()) {
    481       std::ostringstream stream;
    482       stream << '^' << quoted(m_index->name()) << '$';
    483       browser->setFilter(stream.str());
    484     }
    485 
    486     return;
    487   }
    488 
    489   const InstallOpts &installOpts = g_reapack->config()->install;
    490 
    491   if(choice == INSTALL_ALL && boost::logic::indeterminate(remote.autoInstall())
    492       && !installOpts.autoInstall) {
    493     const int btn = Win32::messageBox(m_dialog->handle(),
    494       "Do you want ReaPack to install new packages from this repository"
    495       " when synchronizing in the future?\n\nThis setting can also be"
    496       " customized globally or on a per-repository basis in"
    497       " ReaPack > Manage repositories.",
    498       "Install all packages in this repository", MB_YESNOCANCEL);
    499 
    500     switch(btn) {
    501     case IDYES:
    502       remote.setAutoInstall(true);
    503       g_reapack->config()->remotes.add(remote);
    504       break;
    505     case IDCANCEL:
    506       return;
    507     }
    508   }
    509 
    510   if(Transaction *tx = g_reapack->setupTransaction())
    511     tx->synchronize(remote, choice == INSTALL_ALL);
    512 
    513   if(!remote.isEnabled()) {
    514     remote.setEnabled(true);
    515     g_reapack->addSetRemote(remote);
    516   }
    517 
    518   g_reapack->commitConfig();
    519 }
    520 
    521 AboutPackageDelegate::AboutPackageDelegate(
    522     const Package *pkg, const VersionName &ver)
    523   : m_package(pkg), m_current(ver),
    524     m_index(pkg->category()->index()->shared_from_this())
    525 {
    526 }
    527 
    528 void AboutPackageDelegate::init(About *dialog)
    529 {
    530   m_dialog = dialog;
    531 
    532   dialog->setTitle(m_package->displayName());
    533   dialog->setMetadata(m_package->metadata());
    534   dialog->setAction("About " + m_index->name());
    535 
    536   dialog->tabs()->addTab({"History",
    537     {dialog->menu()->handle(), dialog->getControl(IDC_CHANGELOG)}});
    538   dialog->tabs()->addTab({"Contents",
    539     {dialog->menu()->handle(), dialog->list()->handle()}});
    540 
    541   dialog->menu()->addColumn({"Version", 142, 0, ListView::VersionType});
    542 
    543   dialog->list()->addColumn({"File", 267});
    544   dialog->list()->addColumn({"Path", 207});
    545   dialog->list()->addColumn({"Action List", 84});
    546 
    547   dialog->menu()->reserveRows(m_package->versions().size());
    548 
    549   for(const Version *ver : m_package->versions()) {
    550     auto row = dialog->menu()->createRow();
    551     row->setCell(0, ver->name().toString(), (void *)&ver->name());
    552 
    553     if(m_current == ver->name())
    554       dialog->menu()->select(row->index());
    555   }
    556 
    557   dialog->menu()->sortByColumn(0, ListView::DescendingOrder);
    558 
    559   if(!dialog->menu()->hasSelection())
    560     dialog->menu()->select(dialog->menu()->rowCount() - 1);
    561 }
    562 
    563 void AboutPackageDelegate::updateList(const int index)
    564 {
    565   constexpr std::pair<Source::Section, const char *> sectionMap[] {
    566     {Source::MainSection,                "Main"},
    567     {Source::MIDIEditorSection,          "MIDI Editor"},
    568     {Source::MIDIInlineEditorSection,    "MIDI Inline Editor"},
    569     {Source::MIDIEventListEditorSection, "MIDI Event List Editor"},
    570     {Source::MediaExplorerSection,       "Media Explorer"},
    571   };
    572 
    573   if(index < 0)
    574     return;
    575 
    576   const Version *ver = m_package->version(index);
    577   std::ostringstream stream;
    578   stream << *ver;
    579   Win32::setWindowText(m_dialog->getControl(IDC_CHANGELOG), stream.str().c_str());
    580 
    581   m_dialog->list()->reserveRows(ver->sources().size());
    582 
    583   for(const Source *src : ver->sources()) {
    584     int sections = src->sections();
    585     std::string actionList;
    586 
    587     if(sections) {
    588       std::vector<std::string> sectionNames;
    589 
    590       for(const auto &[section, name] : sectionMap) {
    591         if(sections & section) {
    592           sectionNames.push_back(name);
    593           sections &= ~section;
    594         }
    595       }
    596 
    597       if(sections) // In case we forgot to add a section to sectionMap!
    598         sectionNames.push_back("Other");
    599 
    600       actionList = "Yes (";
    601       actionList += boost::algorithm::join(sectionNames, ", ");
    602       actionList += ')';
    603     }
    604     else
    605       actionList = "No";
    606 
    607     int c = 0;
    608     auto row = m_dialog->list()->createRow((void *)src);
    609     row->setCell(c++, src->targetPath().basename());
    610     row->setCell(c++, src->targetPath().dirname().join());
    611     row->setCell(c++, actionList);
    612   }
    613 }
    614 
    615 bool AboutPackageDelegate::fillContextMenu(Menu &menu, const int index) const
    616 {
    617   if(index < 0)
    618     return false;
    619 
    620   auto src = static_cast<const Source *>(m_dialog->list()->row(index)->userData);
    621 
    622   menu.addAction("Copy source URL", ACTION_COPY_URL);
    623   menu.setEnabled(m_current.size() > 0 && FS::exists(src->targetPath()),
    624     menu.addAction("Locate in explorer/finder", ACTION_LOCATE));
    625 
    626   return true;
    627 }
    628 
    629 void AboutPackageDelegate::onCommand(const int id)
    630 {
    631   switch(id) {
    632   case IDC_ACTION:
    633     m_dialog->setDelegate(std::make_shared<AboutIndexDelegate>(m_index));
    634     break;
    635   case ACTION_COPY_URL:
    636     copySourceUrl();
    637     break;
    638   case ACTION_LOCATE:
    639     locate();
    640     break;
    641   }
    642 }
    643 
    644 const Source *AboutPackageDelegate::currentSource() const
    645 {
    646   const int index = m_dialog->list()->currentIndex();
    647 
    648   if(index < 0)
    649     return nullptr;
    650   else
    651     return static_cast<const Source *>(m_dialog->list()->row(index)->userData);
    652 }
    653 
    654 void AboutPackageDelegate::copySourceUrl()
    655 {
    656   if(const Source *src = currentSource())
    657     m_dialog->setClipboard(src->url());
    658 }
    659 
    660 void AboutPackageDelegate::locate()
    661 {
    662   if(const Source *src = currentSource()) {
    663     const Path &path = src->targetPath();
    664 
    665     if(!FS::exists(path))
    666       return;
    667 
    668     const std::string &arg = String::format(R"(/select,"%s")",
    669       path.prependRoot().join().c_str());
    670 
    671     Win32::shellExecute("explorer.exe", arg.c_str());
    672   }
    673 }