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 }