manager.cpp (17876B)
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 "manager.hpp" 19 20 #include "about.hpp" 21 #include "archive.hpp" 22 #include "config.hpp" 23 #include "errors.hpp" 24 #include "filedialog.hpp" 25 #include "import.hpp" 26 #include "listview.hpp" 27 #include "menu.hpp" 28 #include "progress.hpp" 29 #include "reapack.hpp" 30 #include "remote.hpp" 31 #include "resource.hpp" 32 #include "transaction.hpp" 33 #include "win32.hpp" 34 35 #include <algorithm> 36 37 static const Win32::char_type *ARCHIVE_FILTER = 38 L("ReaPack Offline Archive (*.ReaPackArchive)\0*.ReaPackArchive\0"); 39 static const Win32::char_type *ARCHIVE_EXT = L("ReaPackArchive"); 40 41 enum { 42 ACTION_UNINSTALL = 80, ACTION_ABOUT, ACTION_REFRESH, ACTION_COPYURL, 43 ACTION_SELECT, ACTION_UNSELECT, ACTION_AUTOINSTALL_GLOBAL, 44 ACTION_AUTOINSTALL_OFF, ACTION_AUTOINSTALL_ON, ACTION_AUTOINSTALL, 45 ACTION_BLEEDINGEDGE, ACTION_PROMPTOBSOLETE, ACTION_SYNONYMS, ACTION_NETCONFIG, 46 ACTION_RESETCONFIG, ACTION_IMPORT_REPO, ACTION_IMPORT_ARCHIVE, 47 ACTION_EXPORT_ARCHIVE, 48 }; 49 50 enum { TIMER_ABOUT = 1, }; 51 52 Manager::Manager() 53 : Dialog(IDD_CONFIG_DIALOG), 54 m_list(nullptr), m_changes(0), m_importing(false) 55 { 56 } 57 58 void Manager::onInit() 59 { 60 Dialog::onInit(); 61 62 auto msize = minimumSize(); 63 msize.y = 210; 64 setMinimumSize(msize); 65 66 m_apply = getControl(IDAPPLY); 67 disable(m_apply); 68 69 m_list = createControl<ListView>(IDC_LIST, ListView::Columns{ 70 {"Name", 155}, 71 {"Index URL", 435}, 72 }); 73 74 m_list->enableIcons(); 75 m_list->onSelect >> std::bind(&Dialog::startTimer, this, 100, TIMER_ABOUT, true); 76 m_list->onIconClick >> std::bind(&Manager::toggleEnabled, this); 77 m_list->onActivate >> std::bind(&Manager::aboutRepo, this, true); 78 m_list->onFillContextMenu >> std::bind(&Manager::fillContextMenu, this, 79 std::placeholders::_1, std::placeholders::_2); 80 81 setAnchor(m_list->handle(), AnchorRight | AnchorBottom); 82 setAnchor(getControl(IDC_IMPORT), AnchorTop | AnchorBottom); 83 setAnchor(getControl(IDC_BROWSE), AnchorTop | AnchorBottom); 84 setAnchor(getControl(IDC_OPTIONS), AnchorTop | AnchorBottom); 85 setAnchor(getControl(IDOK), AnchorAll); 86 setAnchor(getControl(IDCANCEL), AnchorAll); 87 setAnchor(m_apply, AnchorAll); 88 89 auto data = m_serializer.read(g_reapack->config()->windowState.manager, 2); 90 restoreState(data); 91 m_list->restoreState(data); 92 93 refresh(); 94 95 m_list->autoSizeHeader(); 96 } 97 98 void Manager::onTimer(const int id) 99 { 100 stopTimer(id); 101 102 if(About *about = g_reapack->about(false)) { 103 if(about->testDelegate<AboutIndexDelegate>()) 104 aboutRepo(false); 105 } 106 } 107 108 void Manager::onClose() 109 { 110 Serializer::Data data; 111 saveState(data); 112 m_list->saveState(data); 113 g_reapack->config()->windowState.manager = m_serializer.write(data); 114 } 115 116 void Manager::onCommand(const int id, int) 117 { 118 switch(id) { 119 case IDC_IMPORT: 120 importExport(); 121 break; 122 case IDC_BROWSE: 123 launchBrowser(); 124 break; 125 case IDC_OPTIONS: 126 options(); 127 break; 128 case ACTION_REFRESH: 129 refreshIndex(); 130 break; 131 case ACTION_AUTOINSTALL_GLOBAL: 132 setRemoteAutoInstall(boost::logic::indeterminate); 133 break; 134 case ACTION_AUTOINSTALL_OFF: 135 setRemoteAutoInstall(false); 136 break; 137 case ACTION_AUTOINSTALL_ON: 138 setRemoteAutoInstall(true); 139 break; 140 case ACTION_COPYURL: 141 copyUrl(); 142 break; 143 case ACTION_UNINSTALL: 144 uninstall(); 145 break; 146 case ACTION_IMPORT_REPO: 147 importRepo(); 148 break; 149 case ACTION_IMPORT_ARCHIVE: 150 importArchive(); 151 break; 152 case ACTION_EXPORT_ARCHIVE: 153 exportArchive(); 154 break; 155 case ACTION_AUTOINSTALL: 156 toggle(m_autoInstall, g_reapack->config()->install.autoInstall); 157 break; 158 case ACTION_BLEEDINGEDGE: 159 toggle(m_bleedingEdge, g_reapack->config()->install.bleedingEdge); 160 break; 161 case ACTION_PROMPTOBSOLETE: 162 toggle(m_promptObsolete, g_reapack->config()->install.promptObsolete); 163 break; 164 case ACTION_SYNONYMS: 165 toggle(m_expandSynonyms, g_reapack->config()->filter.expandSynonyms); 166 break; 167 case ACTION_NETCONFIG: 168 setupNetwork(); 169 break; 170 case ACTION_RESETCONFIG: 171 g_reapack->config()->resetOptions(); 172 g_reapack->config()->restoreDefaultRemotes(); 173 refresh(); 174 break; 175 case ACTION_SELECT: 176 m_list->selectAll(); 177 SetFocus(m_list->handle()); 178 break; 179 case ACTION_UNSELECT: 180 m_list->unselectAll(); 181 SetFocus(m_list->handle()); 182 break; 183 case IDOK: 184 case IDAPPLY: 185 if(confirm()) { 186 if(!apply() || id == IDAPPLY) 187 break; 188 189 // IDOK -> continue to next case (IDCANCEL) 190 } 191 else { 192 setChange(-static_cast<int>(m_uninstall.size())); 193 m_uninstall.clear(); 194 refresh(); 195 break; 196 } 197 [[fallthrough]]; 198 case IDCANCEL: 199 close(); 200 break; 201 default: 202 const int action = id >> 8; 203 if(action == ACTION_ABOUT) 204 g_reapack->about(getRemote(id & 0xff)); 205 break; 206 } 207 } 208 209 bool Manager::fillContextMenu(Menu &menu, const int index) const 210 { 211 const Remote &remote = getRemote(index); 212 213 if(!remote) { 214 menu.addAction("&Select all", ACTION_SELECT); 215 menu.addAction("&Unselect all", ACTION_UNSELECT); 216 return true; 217 } 218 219 menu.addAction("&Refresh", ACTION_REFRESH); 220 menu.addAction("&Copy URL", ACTION_COPYURL); 221 222 Menu autoInstallMenu = menu.addMenu("&Install new packages"); 223 const UINT autoInstallGlobal = autoInstallMenu.addAction( 224 "Use &global setting", ACTION_AUTOINSTALL_GLOBAL); 225 const UINT autoInstallOff = autoInstallMenu.addAction( 226 "Manually", ACTION_AUTOINSTALL_OFF); 227 const UINT autoInstallOn = autoInstallMenu.addAction( 228 "When synchronizing", ACTION_AUTOINSTALL_ON); 229 230 const UINT uninstallAction = 231 menu.addAction("&Uninstall", ACTION_UNINSTALL); 232 233 menu.addSeparator(); 234 235 menu.addAction(String::format("&About %s", remote.name().c_str()), 236 index | (ACTION_ABOUT << 8)); 237 238 bool allProtected = true; 239 bool allAutoInstallGlobal = true; 240 bool allAutoInstallOff = true; 241 bool allAutoInstallOn = true; 242 243 for(const int i : m_list->selection()) { 244 const Remote &r = getRemote(i); 245 if(!r.isProtected()) 246 allProtected = false; 247 248 const tribool &autoInstall = remoteAutoInstall(r); 249 if(boost::logic::indeterminate(autoInstall)) { 250 allAutoInstallOff = false; 251 allAutoInstallOn = false; 252 } 253 else { 254 allAutoInstallGlobal = false; 255 if(autoInstall) 256 allAutoInstallOff = false; 257 else if(!autoInstall) 258 allAutoInstallOn = false; 259 } 260 }; 261 262 if(allProtected) 263 menu.disable(uninstallAction); 264 265 if(allAutoInstallGlobal) 266 autoInstallMenu.check(autoInstallGlobal); 267 else if(allAutoInstallOff) 268 autoInstallMenu.check(autoInstallOff); 269 else if(allAutoInstallOn) 270 autoInstallMenu.check(autoInstallOn); 271 272 return true; 273 } 274 275 bool Manager::onKeyDown(const int key, const int mods) 276 { 277 if(GetFocus() != m_list->handle()) 278 return false; 279 280 if(mods == CtrlModifier && key == 'A') 281 m_list->selectAll(); 282 else if(mods == (CtrlModifier | ShiftModifier) && key == 'A') 283 m_list->unselectAll(); 284 else if(mods == CtrlModifier && key == 'C') 285 copyUrl(); 286 else if(!mods && key == VK_SPACE) 287 toggleEnabled(); 288 else 289 return false; 290 291 return true; 292 } 293 294 void Manager::refresh() 295 { 296 ListView::BeginEdit edit(m_list); 297 298 const std::vector<int> selection = m_list->selection(); 299 std::vector<std::string> selected(selection.size()); 300 for(size_t i = 0; i < selection.size(); i++) 301 selected[i] = m_list->row(selection[i])->cell(0).value; // TODO: use data ptr to Remote 302 303 const auto &remotes = g_reapack->config()->remotes; 304 305 m_list->clear(); 306 m_list->reserveRows(remotes.size()); 307 308 for(const Remote &remote : remotes) { 309 if(m_uninstall.count(remote)) 310 continue; 311 312 int c = 0; 313 auto row = m_list->createRow(); 314 row->setChecked(isRemoteEnabled(remote)); 315 row->setCell(c++, remote.name()); 316 row->setCell(c++, remote.url()); 317 318 if(find(selected.begin(), selected.end(), remote.name()) != selected.end()) 319 m_list->select(row->index()); 320 } 321 } 322 323 void Manager::setMods(const ModsCallback &cb) 324 { 325 ListView::BeginEdit edit(m_list); 326 327 for(const int index : m_list->selection()) { 328 const Remote &remote = getRemote(index); 329 330 auto it = m_mods.find(remote); 331 332 if(it == m_mods.end()) { 333 RemoteMods mods; 334 cb(remote, index, &mods); 335 336 if(!mods) 337 continue; 338 339 m_mods.insert({remote, mods}); 340 setChange(1); 341 } 342 else { 343 RemoteMods *mods = &it->second; 344 cb(remote, index, mods); 345 346 if(!*mods) { 347 m_mods.erase(it); 348 setChange(-1); 349 } 350 } 351 } 352 } 353 354 void Manager::toggleEnabled() 355 { 356 setMods([=](const Remote &remote, const int index, RemoteMods *mods) { 357 const bool enable = !mods->enable.value_or(remote.isEnabled()); 358 359 if(remote.isEnabled() == enable) 360 mods->enable = std::nullopt; 361 else 362 mods->enable = enable; 363 364 m_list->row(index)->setChecked(enable); 365 }); 366 } 367 368 bool Manager::isRemoteEnabled(const Remote &remote) const 369 { 370 const auto &it = m_mods.find(remote); 371 372 if(it == m_mods.end()) 373 return remote.isEnabled(); 374 else 375 return it->second.enable.value_or(remote.isEnabled()); 376 } 377 378 void Manager::setRemoteAutoInstall(const tribool &enabled) 379 { 380 setMods([=](const Remote &remote, int, RemoteMods *mods) { 381 if(remote.autoInstall() == enabled 382 || (indeterminate(remote.autoInstall()) && indeterminate(enabled))) 383 mods->autoInstall = std::nullopt; 384 else 385 mods->autoInstall = enabled; 386 }); 387 } 388 389 tribool Manager::remoteAutoInstall(const Remote &remote) const 390 { 391 const auto &it = m_mods.find(remote); 392 393 if(it == m_mods.end()) 394 return remote.autoInstall(); 395 else 396 return it->second.autoInstall.value_or(remote.autoInstall()); 397 } 398 399 void Manager::refreshIndex() 400 { 401 if(!m_list->hasSelection()) 402 return; 403 404 const std::vector<int> selection = m_list->selection(); 405 std::vector<Remote> remotes(selection.size()); 406 for(size_t i = 0; i < selection.size(); i++) 407 remotes[i] = getRemote(selection[i]); 408 409 if(Transaction *tx = g_reapack->setupTransaction()) { 410 tx->fetchIndexes(remotes, true); 411 tx->runTasks(); 412 } 413 } 414 415 void Manager::uninstall() 416 { 417 int keep = 0; 418 419 while(m_list->selectionSize() - keep > 0) { 420 const int index = m_list->currentIndex() + keep; 421 const Remote &remote = getRemote(index); 422 423 if(remote.isProtected()) { 424 keep++; 425 continue; 426 } 427 428 m_uninstall.insert(remote); 429 setChange(1); 430 431 m_list->removeRow(index); 432 } 433 } 434 435 void Manager::toggle(std::optional<bool> &setting, const bool current) 436 { 437 setting = !setting.value_or(current); 438 setChange(*setting == current ? -1 : 1); 439 } 440 441 void Manager::setChange(const int increment) 442 { 443 if(!m_changes && increment < 0) 444 return; 445 446 m_changes += increment; 447 448 if(m_changes) 449 enable(m_apply); 450 else 451 disable(m_apply); 452 } 453 454 void Manager::copyUrl() 455 { 456 std::vector<std::string> values; 457 458 for(const int index : m_list->selection(false)) 459 values.push_back(getRemote(index).url()); 460 461 setClipboard(values); 462 } 463 464 void Manager::aboutRepo(const bool focus) 465 { 466 if(m_list->hasSelection()) 467 g_reapack->about(getRemote(m_list->currentIndex()), focus); 468 } 469 470 void Manager::importExport() 471 { 472 Menu menu; 473 menu.addAction("Import &repositories...", ACTION_IMPORT_REPO); 474 menu.addSeparator(); 475 menu.addAction("Import offline archive...", ACTION_IMPORT_ARCHIVE); 476 menu.addAction("&Export offline archive...", ACTION_EXPORT_ARCHIVE); 477 478 menu.show(getControl(IDC_IMPORT), handle()); 479 } 480 481 bool Manager::importRepo() 482 { 483 if(m_importing) // avoid opening the import dialog twice on windows 484 return true; 485 486 m_importing = true; 487 const auto ret = Dialog::Show<Import>(instance(), handle()); 488 m_importing = false; 489 490 return ret != 0; 491 } 492 493 void Manager::importArchive() 494 { 495 const char *title = "Import offline archive"; 496 497 const std::string &path = FileDialog::getOpenFileName(handle(), instance(), 498 title, Path::DATA.prependRoot(), ARCHIVE_FILTER, ARCHIVE_EXT); 499 500 if(path.empty()) 501 return; 502 503 try { 504 Archive::import(path); 505 } 506 catch(const reapack_error &e) { 507 Win32::messageBox(handle(), String::format( 508 "An error occured while reading %s.\n\n%s.", path.c_str(), e.what() 509 ).c_str(), title, MB_OK); 510 } 511 } 512 513 void Manager::exportArchive() 514 { 515 const std::string &path = FileDialog::getSaveFileName(handle(), instance(), 516 "Export offline archive", Path::DATA.prependRoot(), ARCHIVE_FILTER, ARCHIVE_EXT); 517 518 if(!path.empty()) { 519 if(Transaction *tx = g_reapack->setupTransaction()) { 520 tx->exportArchive(path); 521 tx->runTasks(); 522 } 523 } 524 } 525 526 void Manager::launchBrowser() 527 { 528 const auto promptApply = [this] { 529 return IDYES == Win32::messageBox(handle(), "Apply unsaved changes?", "ReaPack Query", MB_YESNO); 530 }; 531 532 if(m_changes && promptApply()) 533 apply(); 534 535 g_reapack->browsePackages(); 536 } 537 538 void Manager::options() 539 { 540 Menu menu; 541 542 UINT index = menu.addAction( 543 "&Install new packages when synchronizing", ACTION_AUTOINSTALL); 544 if(m_autoInstall.value_or(g_reapack->config()->install.autoInstall)) 545 menu.check(index); 546 547 index = menu.addAction( 548 "Enable &pre-releases globally (bleeding edge)", ACTION_BLEEDINGEDGE); 549 if(m_bleedingEdge.value_or(g_reapack->config()->install.bleedingEdge)) 550 menu.check(index); 551 552 index = menu.addAction( 553 "Prompt to uninstall obsolete packages", ACTION_PROMPTOBSOLETE); 554 if(m_promptObsolete.value_or(g_reapack->config()->install.promptObsolete)) 555 menu.check(index); 556 557 index = menu.addAction( 558 "Search for synonyms of common words", ACTION_SYNONYMS); 559 if(m_expandSynonyms.value_or(g_reapack->config()->filter.expandSynonyms)) 560 menu.check(index); 561 562 menu.addAction("&Network settings...", ACTION_NETCONFIG); 563 564 menu.addSeparator(); 565 566 menu.addAction("&Restore default settings", ACTION_RESETCONFIG); 567 568 menu.show(getControl(IDC_OPTIONS), handle()); 569 } 570 571 void Manager::setupNetwork() 572 { 573 if(IDOK == Dialog::Show<NetworkConfig>(instance(), handle(), &g_reapack->config()->network)) 574 g_reapack->config()->write(); 575 } 576 577 bool Manager::confirm() const 578 { 579 const size_t uninstallCount = m_uninstall.size(); 580 581 if(!uninstallCount) 582 return true; 583 584 return IDYES == Win32::messageBox(handle(), String::format( 585 "Uninstall %zu %s?\n" 586 "Every file they contain will be removed from your computer.", 587 uninstallCount, uninstallCount == 1 ? "repository" : "repositories" 588 ).c_str(), "ReaPack Query", MB_YESNO); 589 } 590 591 bool Manager::apply() 592 { 593 if(!m_changes) 594 return true; 595 596 Transaction *tx = g_reapack->setupTransaction(); 597 598 if(!tx) 599 return false; 600 601 // syncList is the list of repos to synchronize for autoinstall 602 // (global or local setting) 603 std::set<Remote> syncList; 604 605 if(m_autoInstall) { 606 g_reapack->config()->install.autoInstall = *m_autoInstall; 607 608 if(*m_autoInstall) { 609 const auto &enabledNow = g_reapack->config()->remotes.getEnabled(); 610 copy(enabledNow.begin(), enabledNow.end(), inserter(syncList, syncList.end())); 611 } 612 } 613 614 if(m_bleedingEdge) 615 g_reapack->config()->install.bleedingEdge = *m_bleedingEdge; 616 617 if(m_promptObsolete) 618 g_reapack->config()->install.promptObsolete = *m_promptObsolete; 619 620 if(m_expandSynonyms) 621 g_reapack->config()->filter.expandSynonyms = *m_expandSynonyms; 622 623 for(const auto &pair : m_mods) { 624 Remote remote = pair.first; 625 const RemoteMods &mods = pair.second; 626 627 if(m_uninstall.find(remote) != m_uninstall.end()) 628 continue; 629 630 if(mods.enable) { 631 remote.setEnabled(*mods.enable); 632 syncList.erase(remote); 633 } 634 635 if(mods.autoInstall) { 636 remote.setAutoInstall(*mods.autoInstall); 637 638 const bool isEnabled = mods.enable.value_or(remote.isEnabled()); 639 640 if(isEnabled && *mods.autoInstall) 641 syncList.insert(remote); 642 } 643 644 g_reapack->addSetRemote(remote); 645 } 646 647 for(const Remote &remote : m_uninstall) { 648 g_reapack->uninstall(remote); 649 syncList.erase(remote); 650 } 651 652 for(const Remote &remote : syncList) 653 tx->synchronize(remote); 654 655 reset(); 656 657 g_reapack->commitConfig(); 658 659 return true; 660 } 661 662 void Manager::reset() 663 { 664 m_mods.clear(); 665 m_uninstall.clear(); 666 m_autoInstall = m_bleedingEdge = m_promptObsolete = 667 m_expandSynonyms = std::nullopt; 668 669 m_changes = 0; 670 disable(m_apply); 671 } 672 673 Remote Manager::getRemote(const int index) const 674 { 675 if(index < 0 || index > m_list->rowCount() - 1) 676 return {}; 677 678 const std::string &remoteName = m_list->row(index)->cell(0).value; 679 return g_reapack->config()->remotes.get(remoteName); 680 } 681 682 NetworkConfig::NetworkConfig(NetworkOpts *opts) 683 : Dialog(IDD_NETCONF_DIALOG), m_opts(opts) 684 { 685 } 686 687 void NetworkConfig::onInit() 688 { 689 Dialog::onInit(); 690 691 m_proxy = getControl(IDC_PROXY); 692 Win32::setWindowText(m_proxy, m_opts->proxy.c_str()); 693 694 m_verifyPeer = getControl(IDC_VERIFYPEER); 695 setChecked(m_opts->verifyPeer, m_verifyPeer); 696 697 m_staleThreshold = getControl(IDC_STALETHRSH); 698 setChecked(m_opts->staleThreshold > 0, m_staleThreshold); 699 } 700 701 void NetworkConfig::onCommand(const int id, int) 702 { 703 switch(id) { 704 case IDOK: 705 apply(); 706 [[fallthrough]]; 707 case IDCANCEL: 708 close(id); 709 break; 710 } 711 } 712 713 void NetworkConfig::apply() 714 { 715 m_opts->proxy = Win32::getWindowText(m_proxy); 716 m_opts->verifyPeer = isChecked(m_verifyPeer); 717 m_opts->staleThreshold = isChecked(m_staleThreshold) 718 ? NetworkOpts::OneWeekThreshold : NetworkOpts::NoThreshold; 719 }