From 10bcacf8c1fb9ffb1573f926b359857ea9217029 Mon Sep 17 00:00:00 2001 From: zetaPRIME Date: Sun, 13 Mar 2022 06:47:17 -0400 Subject: [PATCH] recent files tracking and menu items! --- notes | 8 ++- xybrid/config/directories.cpp | 3 +- xybrid/config/directories.h | 1 + xybrid/config/uistate.cpp | 20 +++++++ xybrid/config/uistate.h | 15 +++++ xybrid/fileops-config.cpp | 109 ++++++++++++++++++++++++++++++++++ xybrid/fileops.cpp | 41 ------------- xybrid/fileops.h | 3 + xybrid/main.cpp | 1 + xybrid/mainwindow.cpp | 68 ++++++++++++++++++--- xybrid/mainwindow.h | 3 + 11 files changed, 221 insertions(+), 51 deletions(-) create mode 100644 xybrid/config/uistate.cpp create mode 100644 xybrid/config/uistate.h create mode 100644 xybrid/fileops-config.cpp diff --git a/notes b/notes index 850c73b..87398c4 100644 --- a/notes +++ b/notes @@ -34,12 +34,18 @@ TODO { immediate frontburner { look into performance/timing/sync of audio engine; it's having buffer issues way more than it feels like it should - recent files list and backend { + - recent files list and backend { settings manager to handle loading+saving separate files for "settings" and "state" probably an "update all relevant windows" thing for recents } + prompt save on... { + new project + open project (any) + close + } + add i/o nodes to default project distortion effect diff --git a/xybrid/config/directories.cpp b/xybrid/config/directories.cpp index a03e05a..5e98391 100644 --- a/xybrid/config/directories.cpp +++ b/xybrid/config/directories.cpp @@ -3,7 +3,8 @@ using namespace Xybrid::Config; #include -const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.json"); +const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.dat"); +const QString Directories::stateFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/state.dat"); QString Directories::projects = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/projects"); QString Directories::presets = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/nodes"); diff --git a/xybrid/config/directories.h b/xybrid/config/directories.h index d045816..4de4a98 100644 --- a/xybrid/config/directories.h +++ b/xybrid/config/directories.h @@ -5,6 +5,7 @@ namespace Xybrid::Config { namespace Directories { const extern QString configFile; + const extern QString stateFile; extern QString projects; extern QString presets; diff --git a/xybrid/config/uistate.cpp b/xybrid/config/uistate.cpp new file mode 100644 index 0000000..521eaa6 --- /dev/null +++ b/xybrid/config/uistate.cpp @@ -0,0 +1,20 @@ +#include "uistate.h" + +#include + +#include "fileops.h" +using namespace Xybrid::Config; + +std::list UIState::recentFiles; + +void UIState::save() { + FileOps::saveUIState(); +} + +void UIState::addRecentFile(const QString &f) { + if (!recentFiles.empty() && recentFiles.front() == f) return; // if it's already the most recent file, skip + recentFiles.remove(f); // remove any existing instance from later in the list + recentFiles.push_front(f); + while (recentFiles.size() > MAX_RECENTS) recentFiles.pop_back(); // trim to max size + save(); // and save changes +} diff --git a/xybrid/config/uistate.h b/xybrid/config/uistate.h new file mode 100644 index 0000000..358102a --- /dev/null +++ b/xybrid/config/uistate.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +namespace Xybrid::Config { + namespace UIState { + const constexpr size_t MAX_RECENTS = 10; + extern std::list recentFiles; + + void save(); + void addRecentFile(const QString& f); + } +} diff --git a/xybrid/fileops-config.cpp b/xybrid/fileops-config.cpp new file mode 100644 index 0000000..1a72227 --- /dev/null +++ b/xybrid/fileops-config.cpp @@ -0,0 +1,109 @@ +#include "fileops.h" + +#include "uisocket.h" + +#include "config/uistate.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define qs QStringLiteral + +using Xybrid::Data::Project; +using Xybrid::Data::Pattern; +using Xybrid::Data::Graph; +using Xybrid::Data::Node; + +namespace FileOps = Xybrid::FileOps; + +using namespace Xybrid::Config; + +void FileOps::loadConfig() { + QFile file(Config::Directories::configFile); + if (file.open(QFile::ReadOnly)) { // file exists! read in + QCborStreamReader read(&file); + auto root = QCborValue::fromCbor(read).toMap(); + file.close(); + + if (auto dirs = root[qs("directories")].toMap(); !dirs.isEmpty()) { + if (auto s = dirs[qs("projects")].toString(); !s.isNull()) Config::Directories::projects = s; + if (auto s = dirs[qs("presets")].toString(); !s.isNull()) Config::Directories::presets = s; + } + + } + + // make sure directories exist + if (auto d = QDir(Config::Directories::projects); !d.exists()) d.mkpath("."); + if (auto d = QDir(Config::Directories::presets); !d.exists()) d.mkpath("."); +} + +void FileOps::saveConfig() { + QFileInfo fi(Config::Directories::configFile); + fi.dir().mkpath("."); // make sure directory exists + + QFile file(fi.filePath()); + if (!file.open(QFile::WriteOnly)) return; + + QCborMap root; + + { + QCborMap dirs; + + dirs[qs("projects")] = Config::Directories::projects; + dirs[qs("presets")] = Config::Directories::presets; + + root[qs("directories")] = dirs; + } + + // write out + QCborStreamWriter w(&file); + root.toCborValue().toCbor(w); + file.close(); +} + +void FileOps::loadUIState() { + QFile file(Config::Directories::stateFile); + if (file.open(QFile::ReadOnly)) { // file exists! read in + QCborStreamReader read(&file); + auto root = QCborValue::fromCbor(read).toMap(); + file.close(); + + if (auto recent = root[qs("recent")].toArray(); !recent.isEmpty()) { + UIState::recentFiles.clear(); + for (auto r : recent) UIState::recentFiles.push_back(r.toString()); + } + + } +} + +void FileOps::saveUIState() { + QFileInfo fi(Config::Directories::stateFile); + fi.dir().mkpath("."); // make sure directory exists + + QFile file(fi.filePath()); + if (!file.open(QFile::WriteOnly)) return; + + QCborMap root; + + { + QCborArray recent; + for (auto& r : UIState::recentFiles) recent.append(r); + root[qs("recent")] = recent; + } + + // write out + QCborStreamWriter w(&file); + root.toCborValue().toCbor(w); + file.close(); +} diff --git a/xybrid/fileops.cpp b/xybrid/fileops.cpp index 8776210..f1db682 100644 --- a/xybrid/fileops.cpp +++ b/xybrid/fileops.cpp @@ -339,45 +339,4 @@ std::shared_ptr FileOps::loadNode(QString fileName, std::shared_ptr return Node::fromCbor(root.at(2), parent); // let Node handle the rest } -void FileOps::loadConfig() { - QFile file(Config::Directories::configFile); - if (file.open(QFile::ReadOnly)) { // file exists! read in - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - auto root = doc.object(); - file.close(); - if (auto dirs = root["directories"].toObject(); !dirs.isEmpty()) { - if (auto s = dirs["projects"].toString(); !s.isNull()) Config::Directories::projects = s; - if (auto s = dirs["presets"].toString(); !s.isNull()) Config::Directories::presets = s; - } - - } - - // make sure directories exist - if (auto d = QDir(Config::Directories::projects); !d.exists()) d.mkpath("."); - if (auto d = QDir(Config::Directories::presets); !d.exists()) d.mkpath("."); -} - -void FileOps::saveConfig() { - QFileInfo fi(Config::Directories::configFile); - fi.dir().mkpath("."); // make sure directory exists - - QFile file(fi.filePath()); - if (!file.open(QFile::WriteOnly)) return; - - QJsonDocument doc; - QJsonObject root; - - { - QJsonObject dirs; - - dirs["projects"] = Config::Directories::projects; - dirs["presets"] = Config::Directories::presets; - - root["directories"] = dirs; - } - - doc.setObject(root); - file.write(doc.toJson(QJsonDocument::Indented)); - file.close(); -} diff --git a/xybrid/fileops.h b/xybrid/fileops.h index 7562528..7523a0e 100644 --- a/xybrid/fileops.h +++ b/xybrid/fileops.h @@ -33,4 +33,7 @@ namespace Xybrid::FileOps { void loadConfig(); void saveConfig(); + void loadUIState(); + void saveUIState(); + } diff --git a/xybrid/main.cpp b/xybrid/main.cpp index c7d8dd6..0a9d013 100644 --- a/xybrid/main.cpp +++ b/xybrid/main.cpp @@ -28,6 +28,7 @@ int main(int argc, char *argv[]) { QFontDatabase::addApplicationFont(":/fonts/Arcon-Rounded-Regular.otf"); Xybrid::FileOps::loadConfig(); + Xybrid::FileOps::loadUIState(); Xybrid::Config::PluginRegistry::init(); Xybrid::Audio::AudioEngine::init(); diff --git a/xybrid/mainwindow.cpp b/xybrid/mainwindow.cpp index f352a8f..18ff768 100644 --- a/xybrid/mainwindow.cpp +++ b/xybrid/mainwindow.cpp @@ -41,6 +41,7 @@ using Xybrid::MainWindow; #include "editing/projectcommands.h" #include "editing/patterncommands.h" +#include "config/uistate.h" #include "config/pluginregistry.h" #include "audio/audioengine.h" @@ -124,6 +125,43 @@ MainWindow::MainWindow(QWidget *parent) : //ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }"); } + { /* Set up recent file entries */ } { + auto fm = ui->menuFile; + + auto bfr = ui->actionNew_Window; + + for (size_t i = 0; i < UIState::MAX_RECENTS; i++) { + auto ac = new QAction(fm); + ac->setVisible(false); + fm->insertAction(bfr, ac); + recentFileActions.push_back(ac); + + QObject::connect(ac, &QAction::triggered, ac, [this, i]() { openRecentProject(i); }); + + } + + fm->insertSeparator(bfr); + + // update list every time we show this menu + QObject::connect(fm, &QMenu::aboutToShow, fm, [this]() { + auto ri = UIState::recentFiles.begin(); + auto sz = UIState::recentFiles.size(); + for (size_t i = 0; i < UIState::MAX_RECENTS; i++) { + auto ac = recentFileActions[i]; + if (isetText(qs("%1 %2").arg(ix, fi.fileName())); + ac->setVisible(true); + ri++; + } else { + ac->setVisible(false); + ac->setText(QString()); + } + } + }); + } + { /* Set up pattern list */ } { // model ui->patternList->setModel(new PatternListModel(ui->patternList, this)); @@ -501,17 +539,30 @@ void MainWindow::menuFileNew() { void MainWindow::menuFileOpen() { if (auto fileName = FileOps::showOpenDialog(this, "Open project...", Config::Directories::projects, FileOps::Filter::project); !fileName.isEmpty()) { - auto np = FileOps::loadProject(fileName); - if (!np) { - QMessageBox::critical(this, "Error", "Error loading project"); - return; - } - if (audioEngine->playingProject() == project) audioEngine->stop(); - project = np; - onNewProjectLoaded(); + openProject(fileName); } } +void MainWindow::openProject(const QString& fileName) { + auto np = FileOps::loadProject(fileName); + if (!np) { + QMessageBox::critical(this, "Error", "Error loading project"); + return; + } + if (audioEngine->playingProject() == project) audioEngine->stop(); + project = np; + UIState::addRecentFile(fileName); + onNewProjectLoaded(); +} + +void MainWindow::openRecentProject(size_t idx) { + if (idx > UIState::recentFiles.size()) return; + auto it = UIState::recentFiles.begin(); + for (size_t i = 0; i < idx; i++) it++; + + openProject(QString(*it)); // need to copy string before opening +} + void MainWindow::menuFileSave() { if (project->fileName.isEmpty()) menuFileSaveAs(); else { @@ -528,6 +579,7 @@ void MainWindow::menuFileSaveAs() { } if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) { FileOps::saveProject(project, fileName); + UIState::addRecentFile(fileName); undoStack->setClean(); updateTitle(); } diff --git a/xybrid/mainwindow.h b/xybrid/mainwindow.h index 44049d0..a62c39a 100644 --- a/xybrid/mainwindow.h +++ b/xybrid/mainwindow.h @@ -30,7 +30,10 @@ namespace Xybrid { std::shared_ptr editingSample; QUndoStack* undoStack; + std::vector recentFileActions; + void openProject(const QString& fileName); + void openRecentProject(size_t idx); void onNewProjectLoaded(); void updatePatternLists(); bool selectPatternForEditing(Data::Pattern*);