Give Effector a version and a changelog, shown from the menu
The version is the project() line of CMakeLists.txt and nowhere else: CMake writes it, with the text of CHANGELOG.md, into a generated core/version.hpp, so the number and the release notes the binary shows are the ones it was built from. effector --version (-V) prints it, and the header-bar menu gains a section with Changelog, a window laying the Markdown out as headings and bullets, and About Effector. The core tests hold the newest changelog entry to the current version, so a bump cannot go in without its notes. Releases are tagged vX.Y.Z; this is 0.5.0, and the changelog reconstructs 0.1.0-0.4.0 from the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
#include "core/project.hpp"
|
||||
#include "core/registry.hpp"
|
||||
#include "core/rng.hpp"
|
||||
#include "core/version.hpp"
|
||||
|
||||
namespace effector {
|
||||
namespace {
|
||||
@@ -1893,6 +1894,8 @@ Gtk::Widget& EffectorWindow::buildMainMenu() {
|
||||
});
|
||||
// Never enabled: it only stands in for an empty list.
|
||||
add_action("no-recent")->set_enabled(false);
|
||||
add_action("show-changelog", sigc::mem_fun(*this, &EffectorWindow::showChangelog));
|
||||
add_action("about", sigc::mem_fun(*this, &EffectorWindow::showAbout));
|
||||
|
||||
auto menu = Gio::Menu::create();
|
||||
auto open = Gio::Menu::create();
|
||||
@@ -1908,6 +1911,10 @@ Gtk::Widget& EffectorWindow::buildMainMenu() {
|
||||
save->append("Save Project", "win.save-project");
|
||||
save->append("Save Project As…", "win.save-project-as");
|
||||
menu->append_section(save);
|
||||
auto help = Gio::Menu::create();
|
||||
help->append("Changelog", "win.show-changelog");
|
||||
help->append("About Effector", "win.about");
|
||||
menu->append_section(help);
|
||||
|
||||
// The shortcuts hang off the window rather than the application, which the
|
||||
// window does not belong to yet while it is being built.
|
||||
@@ -2071,6 +2078,153 @@ void EffectorWindow::rebuildRecentMenu() {
|
||||
if (recent.empty()) recentMenu_->append("No Recent Projects", "win.no-recent");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ changelog ---
|
||||
|
||||
namespace {
|
||||
|
||||
// Lays CHANGELOG.md out in a text buffer. It reads only the little Markdown
|
||||
// the file uses -- headings, bullets wrapped onto indented lines, and `code`
|
||||
// -- which is all a changelog needs.
|
||||
void fillChangelog(const Glib::RefPtr<Gtk::TextBuffer>& buffer, std::string_view markdown) {
|
||||
auto tags = buffer->get_tag_table();
|
||||
auto tag = [&](const char* name, auto&& configure) {
|
||||
auto made = Gtk::TextTag::create(name);
|
||||
configure(*made);
|
||||
tags->add(made);
|
||||
};
|
||||
tag("title", [](Gtk::TextTag& t) {
|
||||
t.property_weight() = Pango::Weight::BOLD;
|
||||
t.property_scale() = 1.6;
|
||||
t.property_pixels_below_lines() = 6;
|
||||
});
|
||||
tag("release", [](Gtk::TextTag& t) {
|
||||
t.property_weight() = Pango::Weight::BOLD;
|
||||
t.property_scale() = 1.3;
|
||||
t.property_pixels_above_lines() = 18;
|
||||
t.property_pixels_below_lines() = 2;
|
||||
});
|
||||
tag("section", [](Gtk::TextTag& t) {
|
||||
t.property_weight() = Pango::Weight::BOLD;
|
||||
t.property_pixels_above_lines() = 8;
|
||||
t.property_pixels_below_lines() = 2;
|
||||
});
|
||||
tag("item", [](Gtk::TextTag& t) {
|
||||
t.property_left_margin() = 28;
|
||||
t.property_indent() = -14;
|
||||
t.property_pixels_below_lines() = 3;
|
||||
});
|
||||
tag("para", [](Gtk::TextTag& t) { t.property_pixels_below_lines() = 6; });
|
||||
tag("code", [](Gtk::TextTag& t) { t.property_family() = "monospace"; });
|
||||
|
||||
// Gather each block -- a heading, a bullet, a paragraph -- whole first,
|
||||
// since a bullet or a paragraph goes on over several lines.
|
||||
struct Block {
|
||||
std::string kind;
|
||||
std::string text;
|
||||
};
|
||||
std::vector<Block> blocks;
|
||||
bool open = false; // the last block still takes continuation lines
|
||||
std::size_t pos = 0;
|
||||
while (pos <= markdown.size()) {
|
||||
auto end = markdown.find('\n', pos);
|
||||
if (end == std::string_view::npos) end = markdown.size();
|
||||
std::string line(markdown.substr(pos, end - pos));
|
||||
pos = end + 1;
|
||||
|
||||
const auto first = line.find_first_not_of(' ');
|
||||
if (first == std::string::npos) {
|
||||
open = false;
|
||||
} else if (line.rfind("# ", 0) == 0) {
|
||||
blocks.push_back({"title", line.substr(2)});
|
||||
open = false;
|
||||
} else if (line.rfind("## ", 0) == 0) {
|
||||
// "[0.5.0] - 2026-09-26" reads as "0.5.0 — 2026-09-26".
|
||||
std::string text = line.substr(3);
|
||||
std::erase(text, '[');
|
||||
std::erase(text, ']');
|
||||
if (const auto dash = text.find(" - "); dash != std::string::npos) {
|
||||
text.replace(dash, 3, " — ");
|
||||
}
|
||||
blocks.push_back({"release", text});
|
||||
open = false;
|
||||
} else if (line.rfind("### ", 0) == 0) {
|
||||
blocks.push_back({"section", line.substr(4)});
|
||||
open = false;
|
||||
} else if (line.rfind("- ", 0) == 0) {
|
||||
blocks.push_back({"item", "• " + line.substr(2)});
|
||||
open = true;
|
||||
} else if (open) {
|
||||
blocks.back().text += " " + line.substr(first);
|
||||
} else {
|
||||
blocks.push_back({"para", line.substr(first)});
|
||||
open = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto at = buffer->end();
|
||||
for (std::size_t i = 0; i < blocks.size(); ++i) {
|
||||
const auto& block = blocks[i];
|
||||
// Split on the backticks: every odd piece is code.
|
||||
std::size_t start = 0;
|
||||
bool code = false;
|
||||
while (start <= block.text.size()) {
|
||||
auto tick = block.text.find('`', start);
|
||||
if (tick == std::string::npos) tick = block.text.size();
|
||||
if (tick > start) {
|
||||
std::vector<Glib::ustring> names{block.kind};
|
||||
if (code) names.emplace_back("code");
|
||||
at = buffer->insert_with_tags_by_name(at, block.text.substr(start, tick - start), names);
|
||||
}
|
||||
start = tick + 1;
|
||||
code = !code;
|
||||
}
|
||||
if (i + 1 < blocks.size()) at = buffer->insert_with_tag(at, "\n", block.kind);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EffectorWindow::showChangelog() {
|
||||
if (!changelogWindow_) {
|
||||
changelogWindow_ = std::make_unique<Gtk::Window>();
|
||||
changelogWindow_->set_title("Changelog");
|
||||
changelogWindow_->set_transient_for(*this);
|
||||
changelogWindow_->set_default_size(560, 640);
|
||||
changelogWindow_->set_hide_on_close(true);
|
||||
|
||||
auto* text = Gtk::make_managed<Gtk::TextView>();
|
||||
text->set_name("changelog");
|
||||
text->set_editable(false);
|
||||
text->set_cursor_visible(false);
|
||||
text->set_wrap_mode(Gtk::WrapMode::WORD);
|
||||
text->set_left_margin(18);
|
||||
text->set_right_margin(18);
|
||||
text->set_top_margin(12);
|
||||
text->set_bottom_margin(18);
|
||||
fillChangelog(text->get_buffer(), kChangelog);
|
||||
|
||||
auto* scroller = Gtk::make_managed<Gtk::ScrolledWindow>();
|
||||
scroller->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
|
||||
scroller->set_child(*text);
|
||||
changelogWindow_->set_child(*scroller);
|
||||
}
|
||||
changelogWindow_->present();
|
||||
}
|
||||
|
||||
void EffectorWindow::showAbout() {
|
||||
if (!aboutDialog_) {
|
||||
aboutDialog_ = std::make_unique<Gtk::AboutDialog>();
|
||||
aboutDialog_->set_transient_for(*this);
|
||||
aboutDialog_->set_modal(true);
|
||||
aboutDialog_->set_hide_on_close(true);
|
||||
aboutDialog_->set_program_name("Effector");
|
||||
aboutDialog_->set_version(std::string(kVersion));
|
||||
aboutDialog_->set_comments("Image effects, cascaded through a chain.");
|
||||
aboutDialog_->set_logo_icon_name("applications-graphics");
|
||||
}
|
||||
aboutDialog_->present();
|
||||
}
|
||||
|
||||
void EffectorWindow::onSaveProject() {
|
||||
if (projectPath_.empty()) {
|
||||
onSaveProjectAs();
|
||||
|
||||
@@ -108,6 +108,8 @@ private:
|
||||
void selectStage(int index);
|
||||
void onStageSelected();
|
||||
Gtk::Widget& buildMainMenu(); // the header menu: new, open, save project
|
||||
void showChangelog();
|
||||
void showAbout();
|
||||
Project currentProject() const;
|
||||
void applyProject(const Project& project, const std::string& path);
|
||||
void clearSources();
|
||||
@@ -209,6 +211,9 @@ private:
|
||||
Gtk::Button* addButton_ = nullptr;
|
||||
// Built fresh with the progress window, and null whenever it is closed.
|
||||
std::unique_ptr<Gtk::Window> loadWindow_;
|
||||
// Opened from the menu on first use, then shown again rather than rebuilt.
|
||||
std::unique_ptr<Gtk::Window> changelogWindow_;
|
||||
std::unique_ptr<Gtk::AboutDialog> aboutDialog_;
|
||||
Gtk::ProgressBar* loadBar_ = nullptr;
|
||||
Gtk::Label* loadFileLabel_ = nullptr;
|
||||
Gtk::Box sourcesSection_; // hidden while the chain needs no sources
|
||||
|
||||
Reference in New Issue
Block a user