reapack

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

commit 3c49cd0e6992bd4da9746bb1551367805a01caa4
parent 3b88d5745a67fd1f135ba41cdf03dde084dd10b8
Author: cfillion <cfillion@users.noreply.github.com>
Date:   Mon, 14 Mar 2016 22:25:03 -0400

implement new "Clean up packages" feature

Diffstat:
Asrc/cleanup.cpp | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cleanup.hpp | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/index.cpp | 22++++++++++++++++++++++
Msrc/index.hpp | 7+++++++
Msrc/listview.cpp | 6++++++
Msrc/listview.hpp | 6++++++
Msrc/main.cpp | 6++++++
Msrc/reapack.cpp | 30+++++++++++++++++++++++++++---
Msrc/reapack.hpp | 6++++++
Msrc/resource.hpp | 1+
Msrc/resource.rc | 14++++++++++++++
Msrc/transaction.cpp | 26+++++++++++++++-----------
Msrc/transaction.hpp | 1+
Mtest/index.cpp | 6+++++-
14 files changed, 350 insertions(+), 15 deletions(-)

diff --git a/src/cleanup.cpp b/src/cleanup.cpp @@ -0,0 +1,174 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2016 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "cleanup.hpp" + +#include "encoding.hpp" +#include "errors.hpp" +#include "index.hpp" +#include "listview.hpp" +#include "menu.hpp" +#include "reapack.hpp" +#include "resource.hpp" + +using namespace std; + +enum { ACTION_SELECT = 300, ACTION_UNSELECT }; + +Cleanup::Cleanup(const std::vector<IndexPtr> &indexes, ReaPack *reapack) + : Dialog(IDD_CLEANUP_DIALOG), m_indexes(indexes), m_reapack(reapack) +{ +} + +void Cleanup::onInit() +{ + (void)m_reapack; + m_ok = getControl(IDOK); + m_stateLabel = getControl(IDC_LABEL2); + + disable(m_ok); + + m_list = createControl<ListView>(IDC_LIST, ListView::Columns{ + {AUTO_STR("Name"), 560}, + }); + + m_list->sortByColumn(0); + m_list->onSelect(bind(&Cleanup::onSelectionChanged, this)); + + try { + populate(); + } + catch(const reapack_error &e) { + const auto_string &desc = make_autostring(e.what()); + auto_char msg[255] = {}; + auto_snprintf(msg, sizeof(msg), + AUTO_STR("The file list is currently unavailable.\x20") + AUTO_STR("Retry later when all installation task are completed.\r\n") + AUTO_STR("\r\nError description: %s"), + desc.c_str()); + MessageBox(handle(), msg, AUTO_STR("AAA"), MB_OK); + } + +#ifdef LVSCW_AUTOSIZE_USEHEADER + m_list->resizeColumn(0, LVSCW_AUTOSIZE_USEHEADER); +#endif + + onSelectionChanged(); + + if(m_list->empty()) + startTimer(10); +} + +void Cleanup::onCommand(const int id) +{ + switch(id) { + case ACTION_SELECT: + m_list->selectAll(); + break; + case ACTION_UNSELECT: + m_list->unselectAll(); + break; + case IDOK: + if(confirm()) + apply(); + else + break; + case IDCANCEL: + close(); + break; + } +} + +void Cleanup::onContextMenu(HWND target, const int x, const int y) +{ + if(target != m_list->handle()) + return; + + Menu menu; + menu.addAction(AUTO_STR("&Select all"), ACTION_SELECT); + menu.addAction(AUTO_STR("&Unselect all"), ACTION_UNSELECT); + menu.show(x, y, handle()); +} + +void Cleanup::onTimer(const int id) +{ + stopTimer(id); + + MessageBox(handle(), AUTO_STR("No orphaned package found!"), + AUTO_STR("ReaPack"), MB_OK); + + close(); +} + +void Cleanup::populate() +{ + Registry reg(Path::prefixRoot(Path::REGISTRY)); + + for(IndexPtr index : m_indexes) { + for(const Registry::Entry &entry : reg.getEntries(index->name())) { + const Category *cat = index->category(entry.category); + + if(cat && cat->package(entry.package)) + continue; + + const string row = entry.remote + "/" + entry.category + "/" + entry.package; + m_list->addRow({make_autostring(row)}); + + m_entries.push_back(entry); + } + } + + m_list->sort(); +} + +void Cleanup::onSelectionChanged() +{ + const int selectionSize = m_list->selectionSize(); + + auto_char state[255] = {}; + auto_snprintf(state, sizeof(state), AUTO_STR("%d of %d package%s selected"), + selectionSize, m_list->rowCount(), + selectionSize == 1 ? AUTO_STR("") : AUTO_STR("s")); + + SetWindowText(m_stateLabel, state); + + setEnabled(selectionSize > 0, m_ok); +} + +bool Cleanup::confirm() const +{ + const int count = m_list->selectionSize(); + + auto_char msg[255] = {}; + auto_snprintf(msg, sizeof(msg), + AUTO_STR("Uninstall %d package%s?\n") + AUTO_STR("Every file they contain will be removed from your computer."), + count, count == 1 ? AUTO_STR("") : AUTO_STR("s")); + + const auto_char *title = AUTO_STR("ReaPack Query"); + const int btn = MessageBox(handle(), msg, title, MB_YESNO); + + return btn == IDYES; +} + +void Cleanup::apply() +{ + for(const int i : m_list->selection()) + m_reapack->uninstall(m_entries[i]); + + m_reapack->runTasks(); +} diff --git a/src/cleanup.hpp b/src/cleanup.hpp @@ -0,0 +1,60 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2016 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef REAPACK_CLEANUP_HPP +#define REAPACK_CLEANUP_HPP + +#include "dialog.hpp" + +#include "registry.hpp" + +#include <memory> +#include <vector> + +class Index; +class ListView; +class ReaPack; + +typedef std::shared_ptr<const Index> IndexPtr; + +class Cleanup : public Dialog { +public: + Cleanup(const std::vector<IndexPtr> &, ReaPack *); + +protected: + void onInit() override; + void onCommand(int) override; + void onContextMenu(HWND, int x, int y) override; + void onTimer(int) override; + +private: + void populate(); + void onSelectionChanged(); + bool confirm() const; + void apply(); + + std::vector<IndexPtr> m_indexes; + ReaPack *m_reapack; + + HWND m_ok; + HWND m_stateLabel; + ListView *m_list; + + std::vector<Registry::Entry> m_entries; +}; + +#endif diff --git a/src/index.cpp b/src/index.cpp @@ -134,12 +134,23 @@ void Index::addCategory(const Category *cat) if(cat->packages().empty()) return; + m_catMap.insert({cat->name(), m_categories.size()}); m_categories.push_back(cat); m_packages.insert(m_packages.end(), cat->packages().begin(), cat->packages().end()); } +const Category *Index::category(const string &name) const +{ + const auto it = m_catMap.find(name); + + if(it == m_catMap.end()) + return nullptr; + else + return category(it->second); +} + void Index::addLink(const LinkType type, const Link &link) { if(boost::algorithm::starts_with(link.url, "http")) @@ -187,5 +198,16 @@ void Category::addPackage(const Package *pkg) else if(pkg->versions().empty()) return; + m_pkgMap.insert({pkg->name(), m_packages.size()}); m_packages.push_back(pkg); } + +const Package *Category::package(const string &name) const +{ + const auto it = m_pkgMap.find(name); + + if(it == m_pkgMap.end()) + return nullptr; + else + return package(it->second); +} diff --git a/src/index.hpp b/src/index.hpp @@ -21,6 +21,7 @@ #include <map> #include <memory> #include <string> +#include <unordered_map> #include <vector> #include "package.hpp" @@ -63,6 +64,7 @@ public: void addCategory(const Category *cat); const CategoryList &categories() const { return m_categories; } const Category *category(size_t i) const { return m_categories[i]; } + const Category *category(const std::string &name) const; const PackageList &packages() const { return m_packages; } @@ -74,6 +76,8 @@ private: LinkMap m_links; CategoryList m_categories; PackageList m_packages; + + std::unordered_map<std::string, size_t> m_catMap; }; class Category { @@ -88,12 +92,15 @@ public: void addPackage(const Package *pack); const PackageList &packages() const { return m_packages; } const Package *package(size_t i) const { return m_packages[i]; } + const Package *package(const std::string &name) const; private: const Index *m_index; std::string m_name; PackageList m_packages; + + std::unordered_map<std::string, size_t> m_pkgMap; }; #endif diff --git a/src/listview.cpp b/src/listview.cpp @@ -183,6 +183,12 @@ void ListView::clear() m_rows.clear(); } +void ListView::setSelected(const int index, const bool select) +{ + ListView_SetItemState(handle(), translate(index), + select ? LVIS_SELECTED : 0, LVIS_SELECTED); +} + bool ListView::hasSelection() const { return selectionSize() > 0; diff --git a/src/listview.hpp b/src/listview.hpp @@ -45,6 +45,11 @@ public: void sort(); void sortByColumn(int index, SortOrder order = AscendingOrder); void clear(); + void setSelected(int index, bool select); + void select(int index) { setSelected(index, true); } + void unselect(int index) { setSelected(index, false); } + void selectAll() { select(-1); } + void unselectAll() { unselect(-1); } int selectionSize() const; bool hasSelection() const; @@ -52,6 +57,7 @@ public: std::vector<int> selection() const; int itemUnderMouse() const; int rowCount() const { return (int)m_rows.size(); } + bool empty() const { return rowCount() < 1; } void onSelect(const VoidSignal::slot_type &slot) { m_onSelect.connect(slot); } void onActivate(const VoidSignal::slot_type &slot) { m_onActivate.connect(slot); } diff --git a/src/main.cpp b/src/main.cpp @@ -84,6 +84,9 @@ static void menuHook(const char *name, HMENU handle, int f) menu.addAction(AUTO_STR("Import a repository..."), NamedCommandLookup("_REAPACK_IMPORT")); + menu.addAction(AUTO_STR("Clean up packages..."), + NamedCommandLookup("_REAPACK_CLEANUP")); + menu.addAction(AUTO_STR("Manage repositories..."), NamedCommandLookup("_REAPACK_MANAGE")); @@ -159,6 +162,9 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT( reapack->setupAction("REAPACK_IMPORT", "ReaPack: Import a repository...", &reapack->importAction, bind(&ReaPack::importRemote, reapack)); + reapack->setupAction("REAPACK_CLEANUP", "ReaPack: Clean up packages...", + &reapack->cleanupAction, bind(&ReaPack::cleanupPackages, reapack)); + reapack->setupAction("REAPACK_MANAGE", "ReaPack: Manage repositories...", &reapack->configAction, bind(&ReaPack::manageRemotes, reapack)); diff --git a/src/reapack.cpp b/src/reapack.cpp @@ -18,6 +18,7 @@ #include "reapack.hpp" #include "about.hpp" +#include "cleanup.hpp" #include "config.hpp" #include "errors.hpp" #include "filesystem.hpp" @@ -61,9 +62,9 @@ static void CleanupTempFiles() #endif ReaPack::ReaPack(REAPER_PLUGIN_HINSTANCE instance) - : syncAction(), importAction(), configAction(), + : syncAction(), importAction(), cleanupAction(), configAction(), m_transaction(nullptr), m_progress(nullptr), m_manager(nullptr), - m_import(nullptr), m_instance(instance) + m_import(nullptr), m_cleanup(nullptr), m_instance(instance) { m_mainWindow = GetMainHwnd(); m_useRootPath = new UseRootPath(GetResourcePath()); @@ -198,6 +199,12 @@ void ReaPack::uninstall(const Remote &remote) }); } +void ReaPack::uninstall(const Registry::Entry &entry) +{ + if(hitchhikeTransaction()) + m_transaction->uninstall(entry); +} + void ReaPack::importRemote() { if(m_import) { @@ -302,10 +309,27 @@ void ReaPack::about(const Remote &remote, HWND parent) void ReaPack::cleanupPackages() { + if(m_cleanup) { + m_cleanup->setFocus(); + return; + } + else if(m_transaction) { + ShowMessageBox( + "This feature cannot be used while packages are being installed. " + "Try again later.", "Clean up packages", MB_OK + ); + return; + } + const vector<Remote> &remotes = m_config->remotes()->getEnabled(); fetchIndexes(remotes, [=] (const vector<IndexPtr> &indexes) { - printf("%zu indexes loaded\n", indexes.size()); + m_cleanup = Dialog::Create<Cleanup>(m_instance, m_mainWindow, indexes, this); + m_cleanup->show(); + m_cleanup->setCloseHandler([=] (INT_PTR) { + Dialog::Destroy(m_cleanup); + m_cleanup = nullptr; + }); }); } diff --git a/src/reapack.hpp b/src/reapack.hpp @@ -19,6 +19,7 @@ #define REAPACK_REAPACK_HPP #include "path.hpp" +#include "registry.hpp" #include <functional> #include <map> @@ -27,6 +28,7 @@ #include <reaper_plugin.h> +class Cleanup; class Config; class DownloadQueue; class Import; @@ -49,6 +51,7 @@ public: gaccel_register_t syncAction; gaccel_register_t importAction; + gaccel_register_t cleanupAction; gaccel_register_t configAction; ReaPack(REAPER_PLUGIN_HINSTANCE); @@ -64,11 +67,13 @@ public: void enable(const Remote &r) { setRemoteEnabled(r, true); } void disable(const Remote &r) { setRemoteEnabled(r, false); } void uninstall(const Remote &); + void uninstall(const Registry::Entry &); void importRemote(); void import(const Remote &); void manageRemotes(); void aboutSelf(); void about(const Remote &, HWND parent); + void cleanupPackages(); void fetchIndexes(const std::vector<Remote> &, const IndexesCallback &, HWND = nullptr); @@ -91,6 +96,7 @@ private: Progress *m_progress; Manager *m_manager; Import *m_import; + Cleanup *m_cleanup; REAPER_PLUGIN_HINSTANCE m_instance; HWND m_mainWindow; diff --git a/src/resource.hpp b/src/resource.hpp @@ -39,6 +39,7 @@ #define IDD_CONFIG_DIALOG 102 #define IDD_ABOUT_DIALOG 103 #define IDD_IMPORT_DIALOG 104 +#define IDD_CLEANUP_DIALOG 105 #define IDC_LABEL 200 #define IDC_LABEL2 201 diff --git a/src/resource.rc b/src/resource.rc @@ -76,3 +76,17 @@ BEGIN DEFPUSHBUTTON "&OK", IDOK, 201, 50, 40, 14 PUSHBUTTON "&Cancel", IDCANCEL, 244, 50, 40, 14 END + +IDD_CLEANUP_DIALOG DIALOGEX 0, 0, 350, 200 +STYLE DIALOG_STYLE +FONT DIALOG_FONT +CAPTION "ReaPack: Clean up packages" +BEGIN + LTEXT "The following packages could not be found in any enabled the repositories:", + IDC_LABEL, 5, 5, 340, 10 + CONTROL "", IDC_LIST, WC_LISTVIEW, LVS_REPORT | LVS_SHOWSELALWAYS | + LVS_NOCOLUMNHEADER | WS_BORDER | WS_TABSTOP, 5, 18, 340, 156 + LTEXT "", IDC_LABEL2, 5, 183, 150, 10 + DEFPUSHBUTTON "&Uninstall selected packages", IDOK, 176, 180, 120, 14 + PUSHBUTTON "&Cancel", IDCANCEL, 299, 180, 45, 14 +END diff --git a/src/transaction.cpp b/src/transaction.cpp @@ -229,22 +229,26 @@ void Transaction::uninstall(const Remote &remote) if(entries.empty()) return; - vector<Path> allFiles; + for(const auto &entry : entries) + uninstall(entry); - for(const auto &entry : entries) { - const set<Path> &files = m_registry->getFiles(entry); - for(const Path &path : files) { - if(FS::exists(path)) - allFiles.push_back(path); - } +} - registerInHost(false, entry); +void Transaction::uninstall(const Registry::Entry &entry) +{ + vector<Path> files; - // forget the package even if some files cannot be removed - m_registry->forget(entry); + for(const Path &path : m_registry->getFiles(entry)) { + if(FS::exists(path)) + files.push_back(path); } - RemoveTask *task = new RemoveTask(allFiles, this); + registerInHost(false, entry); + + // forget the package even if some files cannot be removed + m_registry->forget(entry); + + RemoveTask *task = new RemoveTask(files, this); task->onCommit([=] { m_receipt.addRemovals(task->removedFiles()); }); addTask(task); diff --git a/src/transaction.hpp b/src/transaction.hpp @@ -59,6 +59,7 @@ public: void synchronize(const Remote &, bool userAction = true); void install(const Version *); void uninstall(const Remote &); + void uninstall(const Registry::Entry &); void registerAll(const Remote &); void runTasks(); diff --git a/test/index.cpp b/test/index.cpp @@ -94,7 +94,7 @@ TEST_CASE("unicode index path", M) { REQUIRE(ri->name() == "Новая папка"); } -TEST_CASE("add category", M) { +TEST_CASE("add a category", M) { Index ri("a"); Category *cat = new Category("a", &ri); Package *pack = new Package(Package::ScriptType, "name", cat); @@ -106,10 +106,12 @@ TEST_CASE("add category", M) { cat->addPackage(pack); CHECK(ri.categories().size() == 0); + CHECK(ri.category("a") == nullptr); ri.addCategory(cat); REQUIRE(ri.categories().size() == 1); + REQUIRE(ri.category("a") == cat); REQUIRE(ri.packages() == cat->packages()); } @@ -145,10 +147,12 @@ TEST_CASE("add a package", M) { pack->addVersion(ver); CHECK(cat.packages().size() == 0); + CHECK(cat.package("name") == nullptr); cat.addPackage(pack); REQUIRE(cat.packages().size() == 1); + REQUIRE(cat.package("name") == pack); REQUIRE(pack->category() == &cat); }