List recently opened projects under Open Recent
Projects opened or saved go into GTK's shared recent list, signed with the effector application name, and the project menu lists the newest ten of them. The GUI tests point XDG_DATA_HOME at a temporary folder so they leave the user's own list alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ const char* const kImagePatterns[] = {"*.png", "*.jpg", "*.jpeg", "*.webp", "*
|
||||
"*.tif", "*.tiff", "*.gif", "*.ppm", "*.PNG",
|
||||
"*.JPG", "*.JPEG", "*.WEBP"};
|
||||
|
||||
// How this program signs the entries it adds to the shared recent list, so
|
||||
// Open Recent lists its projects and nothing else.
|
||||
const char* const kRecentApp = "effector";
|
||||
const std::size_t kRecentCount = 10;
|
||||
|
||||
Glib::RefPtr<Gio::ListStore<Gtk::FileFilter>> projectFilters() {
|
||||
auto filter = Gtk::FileFilter::create();
|
||||
filter->set_name("Effector projects");
|
||||
@@ -108,6 +113,7 @@ EffectorWindow::EffectorWindow(const Options& initial) {
|
||||
EffectorWindow::~EffectorWindow() {
|
||||
debounce_.disconnect();
|
||||
chainSplitFit_.disconnect();
|
||||
recentChanged_.disconnect();
|
||||
loadWindowDelay_.disconnect();
|
||||
loadCancel_.store(true, std::memory_order_relaxed);
|
||||
if (loader_.joinable()) loader_.join();
|
||||
@@ -1877,11 +1883,26 @@ Gtk::Widget& EffectorWindow::buildMainMenu() {
|
||||
add_action("open-project", sigc::mem_fun(*this, &EffectorWindow::onOpenProject));
|
||||
add_action("save-project", sigc::mem_fun(*this, &EffectorWindow::onSaveProject));
|
||||
add_action("save-project-as", sigc::mem_fun(*this, &EffectorWindow::onSaveProjectAs));
|
||||
// The entry carries the file's URI rather than its path: a target must be
|
||||
// UTF-8, and a file name need not be.
|
||||
add_action_with_parameter("open-recent", Glib::VARIANT_TYPE_STRING,
|
||||
[this](const Glib::VariantBase& target) {
|
||||
const auto uri =
|
||||
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(target).get();
|
||||
openProject(Glib::filename_from_uri(uri));
|
||||
});
|
||||
// Never enabled: it only stands in for an empty list.
|
||||
add_action("no-recent")->set_enabled(false);
|
||||
|
||||
auto menu = Gio::Menu::create();
|
||||
auto open = Gio::Menu::create();
|
||||
open->append("New Project", "win.new-project");
|
||||
open->append("Open Project…", "win.open-project");
|
||||
recentMenu_ = Gio::Menu::create();
|
||||
open->append_submenu("Open Recent", recentMenu_);
|
||||
rebuildRecentMenu();
|
||||
recentChanged_ = Gtk::RecentManager::get_default()->signal_changed().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::rebuildRecentMenu));
|
||||
menu->append_section(open);
|
||||
auto save = Gio::Menu::create();
|
||||
save->append("Save Project", "win.save-project");
|
||||
@@ -1988,11 +2009,66 @@ void EffectorWindow::onProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& resul
|
||||
return; // cancelled
|
||||
}
|
||||
|
||||
openProject(path);
|
||||
}
|
||||
|
||||
void EffectorWindow::openProject(const std::string& path) {
|
||||
try {
|
||||
applyProject(loadProject(path), path);
|
||||
} catch (const std::exception& error) {
|
||||
showError(std::string("Could not open the project: ") + error.what());
|
||||
return;
|
||||
}
|
||||
rememberProject(path);
|
||||
}
|
||||
|
||||
void EffectorWindow::rememberProject(const std::string& path) {
|
||||
Gtk::RecentManager::Data data;
|
||||
data.app_name = kRecentApp;
|
||||
data.app_exec = "effector";
|
||||
data.mime_type = "application/json";
|
||||
try {
|
||||
Gtk::RecentManager::get_default()->add_item(
|
||||
Glib::filename_to_uri(std::filesystem::absolute(path).string()), data);
|
||||
} catch (const Glib::Error&) {
|
||||
// Not a path a URI can name; the project is open all the same.
|
||||
}
|
||||
}
|
||||
|
||||
void EffectorWindow::rebuildRecentMenu() {
|
||||
std::vector<Glib::RefPtr<Gtk::RecentInfo>> recent;
|
||||
for (const auto& info : Gtk::RecentManager::get_default()->get_items()) {
|
||||
if (info->has_application(kRecentApp) && info->exists()) recent.push_back(info);
|
||||
}
|
||||
std::sort(recent.begin(), recent.end(), [](const auto& a, const auto& b) {
|
||||
return a->get_modified().compare(b->get_modified()) > 0;
|
||||
});
|
||||
if (recent.size() > kRecentCount) recent.resize(kRecentCount);
|
||||
|
||||
// A project is listed by its name, and by its folder too only where two
|
||||
// of the same name would otherwise read alike.
|
||||
std::vector<std::filesystem::path> paths;
|
||||
for (const auto& info : recent) paths.emplace_back(Glib::filename_from_uri(info->get_uri()));
|
||||
recentMenu_->remove_all();
|
||||
for (std::size_t i = 0; i < recent.size(); ++i) {
|
||||
std::string label = paths[i].stem().string();
|
||||
const auto sameName = std::count_if(paths.begin(), paths.end(), [&](const auto& other) {
|
||||
return other.stem() == paths[i].stem();
|
||||
});
|
||||
if (sameName > 1) label += " (" + paths[i].parent_path().filename().string() + ")";
|
||||
// The menu reads an underscore as a mnemonic.
|
||||
const std::string display = Glib::filename_display_name(label);
|
||||
std::string escaped;
|
||||
for (const char c : display) {
|
||||
escaped += c;
|
||||
if (c == '_') escaped += '_';
|
||||
}
|
||||
auto item = Gio::MenuItem::create(escaped, "");
|
||||
item->set_action_and_target("win.open-recent",
|
||||
Glib::Variant<Glib::ustring>::create(recent[i]->get_uri()));
|
||||
recentMenu_->append_item(item);
|
||||
}
|
||||
if (recent.empty()) recentMenu_->append("No Recent Projects", "win.no-recent");
|
||||
}
|
||||
|
||||
void EffectorWindow::onSaveProject() {
|
||||
@@ -2039,6 +2115,7 @@ void EffectorWindow::writeProject(const std::string& path) {
|
||||
clearError();
|
||||
projectPath_ = path;
|
||||
updateTitle();
|
||||
rememberProject(path);
|
||||
status_.set_text("Saved " + path);
|
||||
status_.set_tooltip_text("Saved " + path);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,11 @@ private:
|
||||
void clearSources();
|
||||
void onNewProject();
|
||||
void onOpenProject();
|
||||
void openProject(const std::string& path);
|
||||
// Open Recent: the projects this program opened or saved, newest first,
|
||||
// kept in the desktop's shared list of recently used files.
|
||||
void rememberProject(const std::string& path);
|
||||
void rebuildRecentMenu();
|
||||
void onProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
|
||||
const Glib::RefPtr<Gtk::FileDialog>& dialog);
|
||||
void onSaveProject(); // falls back to Save As while the project is untitled
|
||||
@@ -167,6 +172,8 @@ private:
|
||||
Image result_;
|
||||
std::uint64_t seed_ = 0;
|
||||
std::string projectPath_; // empty until the project has been saved once
|
||||
Glib::RefPtr<Gio::Menu> recentMenu_;
|
||||
sigc::connection recentChanged_; // the recent list outlives the window
|
||||
bool suspendRender_ = true;
|
||||
|
||||
// Source loading. Decoding a batch of large photographs takes seconds, so
|
||||
|
||||
Reference in New Issue
Block a user