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:
@@ -0,0 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
Every release of Effector, newest first. The version lives in one place, the
|
||||
`project()` line of `CMakeLists.txt`, and each release is a `vX.Y.Z` tag on the
|
||||
commit that made it. The interface shows this file under Changelog in its menu.
|
||||
|
||||
## [0.5.0] - 2026-09-26
|
||||
|
||||
### Added
|
||||
- A version number: `effector --version` prints it, and About Effector in the
|
||||
menu shows it.
|
||||
- This changelog, readable from the interface under Changelog in the menu.
|
||||
- Open Recent in the project menu, listing the projects opened or saved here.
|
||||
- An optional shadow or glow under the image overlay, with an offset, size,
|
||||
spread, colour and opacity.
|
||||
- Reordering the chain by dragging a row.
|
||||
|
||||
### Fixed
|
||||
- The params' scrollbar no longer covers the spin buttons.
|
||||
|
||||
## [0.4.0] - 2026-09-23
|
||||
|
||||
### Added
|
||||
- Eight chaotic effects: slit shift, channel split, pixel sort, corruption,
|
||||
shatter, warp, bit crush and feedback. None needs sources, so any of them can
|
||||
lead a chain.
|
||||
- The sidebar split into Chain and Setup tabs.
|
||||
- A rotation for the text effect's placement.
|
||||
- Text stages labelled in the chain list with their words.
|
||||
|
||||
### Changed
|
||||
- The preview stays centred however long the status line gets.
|
||||
- The status line stays short, with the full chain and setup in its tooltip.
|
||||
- The chain/params split fits the chain's rows until it is dragged; a
|
||||
double-click on the handle fits it again.
|
||||
- The mouse wheel scrolls past spin buttons that do not have the focus.
|
||||
|
||||
## [0.3.0] - 2026-09-19
|
||||
|
||||
### Added
|
||||
- Named styles: several text stages can share one font and colour, and editing
|
||||
the style moves every stage that follows it.
|
||||
- Switching a chain stage off without taking it apart, in the interface and
|
||||
with `--disable NAME` on the command line.
|
||||
|
||||
### Changed
|
||||
- The interface opens on an empty chain, and shows the sources only when the
|
||||
chain uses them.
|
||||
|
||||
## [0.2.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
- Effect chains: stages cascade, each handed the previous one's output, built in
|
||||
the sidebar or with repeated `-e`.
|
||||
- Blur, film grain, black and white, image overlay, text and tiles effects.
|
||||
- Projects saved and opened as plain JSON.
|
||||
- Standard web and print canvas formats (`--format`, `--dpi`).
|
||||
- Effects declare whether they need source images, so a chain led by blur
|
||||
renders with none loaded.
|
||||
|
||||
### Changed
|
||||
- The chain comes first in the sidebar.
|
||||
|
||||
### Fixed
|
||||
- The canvas size is checked before it is narrowed to an int.
|
||||
- Projects are written and read in the C locale, and keep the seed whole.
|
||||
|
||||
## [0.1.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
- The C++20 port of the Python prototype: a GTK 4 interface and a headless
|
||||
command line over pluggable effects, starting with scanlines.
|
||||
- Source images load in the background, with a progress window and Cancel.
|
||||
@@ -33,6 +33,20 @@ Headless render (no display needed, the fastest end-to-end smoke test):
|
||||
build/effector -o /tmp/out.png -s 800x600 --strip-height 6 --seed 1 a.jpg b.png
|
||||
```
|
||||
|
||||
### Versions and the changelog
|
||||
|
||||
The version is the `project()` line of `CMakeLists.txt`, and nowhere else.
|
||||
CMake writes it, together with the text of `CHANGELOG.md`, into a generated
|
||||
`core/version.hpp` (`kVersion`, `kChangelog`, from `src/core/version.hpp.in`)
|
||||
in the build tree. `--version` prints it, and the header-bar menu shows it
|
||||
under About Effector and the changelog under Changelog. The `version` core test
|
||||
fails unless the newest `## [X.Y.Z]` entry in `CHANGELOG.md` is the current
|
||||
version, so a bump and its changelog entry go in together. Each release is an
|
||||
annotated `vX.Y.Z` tag on the commit that made it. When you add a user-visible
|
||||
change, add a line to the newest changelog entry, or start a new entry if that
|
||||
one is already tagged. The Changelog window reads only headings, `- ` bullets
|
||||
(wrapped onto indented lines) and `` `code` ``, so keep the file to those.
|
||||
|
||||
### Running the GUI from this shell
|
||||
|
||||
GTK picks Wayland from an inherited `WAYLAND_DISPLAY` and will not draw into an
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(effector VERSION 0.1.0 LANGUAGES CXX)
|
||||
project(effector VERSION 0.5.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -21,6 +21,14 @@ pkg_check_modules(PANGOCAIRO REQUIRED IMPORTED_TARGET pangocairo)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# --- version: the one number, and the changelog that goes with it ------------
|
||||
# The version is the project() line above; each release is a vX.Y.Z tag on the
|
||||
# commit that set it. CHANGELOG.md is baked in so the interface can show it,
|
||||
# and editing it re-runs the configure step.
|
||||
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/CHANGELOG.md EFFECTOR_CHANGELOG)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS CHANGELOG.md)
|
||||
configure_file(src/core/version.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/core/version.hpp @ONLY)
|
||||
|
||||
# --- core: effects and image handling, no GUI ---------------------------------
|
||||
add_library(effector_core STATIC
|
||||
src/core/canvas.cpp
|
||||
@@ -48,7 +56,7 @@ add_library(effector_core STATIC
|
||||
src/effects/tiles.cpp
|
||||
src/effects/warp.cpp
|
||||
)
|
||||
target_include_directories(effector_core PUBLIC src)
|
||||
target_include_directories(effector_core PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||
# The core is GUI-free: it only needs gdk-pixbuf for decoding and encoding,
|
||||
# so headless rendering works without a display.
|
||||
target_link_libraries(effector_core PUBLIC PkgConfig::GDKPIXBUF PkgConfig::PANGOCAIRO Threads::Threads)
|
||||
|
||||
@@ -25,6 +25,7 @@ installs it to `~/.local/bin`.
|
||||
build/effector # GTK interface
|
||||
build/effector --list-effects
|
||||
build/effector --help
|
||||
build/effector --version
|
||||
```
|
||||
|
||||
Headless rendering, no display needed:
|
||||
@@ -60,6 +61,17 @@ batch. Small batches finish before the window would be drawn, so it only appears
|
||||
if the decode is still going after 250 ms. On a terminal the headless mode keeps
|
||||
a single `reading n/total` line updated on stderr; redirected output stays clean.
|
||||
|
||||
## Versions
|
||||
|
||||
Releases are listed in [CHANGELOG.md](CHANGELOG.md) and tagged `vX.Y.Z`
|
||||
(`git tag -l 'v*'`). `effector --version` prints the version, and the menu in
|
||||
the interface has Changelog and About Effector.
|
||||
|
||||
To cut a release: bump the `project()` version in `CMakeLists.txt`, move the
|
||||
changes into a new `## [X.Y.Z] - date` entry at the top of `CHANGELOG.md` (the
|
||||
core tests fail until the two agree), commit, and tag it:
|
||||
`git tag -a vX.Y.Z -m "Effector X.Y.Z"`.
|
||||
|
||||
## Effects
|
||||
|
||||
### scanlines
|
||||
|
||||
@@ -61,6 +61,8 @@ Options parseOptions(int argc, char** argv) {
|
||||
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
options.showHelp = true;
|
||||
} else if (arg == "-V" || arg == "--version") {
|
||||
options.showVersion = true;
|
||||
} else if (arg == "--list-effects") {
|
||||
options.listEffects = true;
|
||||
} else if (arg == "-e" || arg == "--effect") {
|
||||
@@ -184,6 +186,7 @@ std::string usage(const char* programName) {
|
||||
<< " --disable NAME keep this effect in the chain but skip it\n"
|
||||
<< " --seed N random seed (default: random)\n"
|
||||
<< " --list-effects list available effects and exit\n"
|
||||
<< " -V, --version show the version and exit\n"
|
||||
<< " -h, --help show this help and exit\n";
|
||||
|
||||
out << "\nCanvas formats (--format):\n";
|
||||
|
||||
@@ -27,6 +27,7 @@ struct Options {
|
||||
ParamValues params; // only the keys the user actually passed
|
||||
bool listEffects = false;
|
||||
bool showHelp = false;
|
||||
bool showVersion = false;
|
||||
};
|
||||
|
||||
// Throws EffectError on bad input. Effect flags are generated from the
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
// Generated by CMake from src/core/version.hpp.in; edit that, not the copy in
|
||||
// the build tree. The version comes from the project() line of CMakeLists.txt
|
||||
// and the changelog from CHANGELOG.md, both read at configure time.
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace effector {
|
||||
|
||||
inline constexpr std::string_view kVersion = "@PROJECT_VERSION@";
|
||||
|
||||
// CHANGELOG.md as it stood when the build was configured, as Markdown.
|
||||
inline constexpr std::string_view kChangelog = R"changelog(@EFFECTOR_CHANGELOG@)changelog";
|
||||
|
||||
} // namespace effector
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/image.hpp"
|
||||
#include "core/registry.hpp"
|
||||
#include "core/rng.hpp"
|
||||
#include "core/version.hpp"
|
||||
#include "gui/app.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -78,6 +79,10 @@ int main(int argc, char** argv) {
|
||||
std::cout << effector::usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
if (options.showVersion) {
|
||||
std::cout << "effector " << effector::kVersion << "\n";
|
||||
return 0;
|
||||
}
|
||||
if (options.listEffects) {
|
||||
for (const auto& effect : effector::effects()) {
|
||||
std::cout << effect->name() << "\t" << effect->title() << " - "
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "core/registry.hpp"
|
||||
#include "core/rng.hpp"
|
||||
#include "core/style.hpp"
|
||||
#include "core/version.hpp"
|
||||
#include "effects/blur.hpp"
|
||||
#include "effects/grain.hpp"
|
||||
#include "effects/monochrome.hpp"
|
||||
@@ -1715,6 +1716,21 @@ int main() {
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
|
||||
std::cout << "version\n";
|
||||
{
|
||||
// A release is the version in CMakeLists.txt and its entry at the top
|
||||
// of CHANGELOG.md; bumping one without the other fails here.
|
||||
const std::string_view log = kChangelog;
|
||||
const auto newest = log.find("\n## [");
|
||||
check(newest != std::string_view::npos &&
|
||||
log.substr(newest + 5).starts_with(std::string(kVersion) + "]"),
|
||||
"the newest changelog entry is this version");
|
||||
const auto options = parsedOptions({"--version"});
|
||||
check(options && options->showVersion, "--version is understood");
|
||||
const auto shortOptions = parsedOptions({"-V"});
|
||||
check(shortOptions && shortOptions->showVersion, "and so is -V");
|
||||
}
|
||||
|
||||
std::cout << (failures == 0 ? "\nall checks passed\n" : "\n" + std::to_string(failures) + " failed\n");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "cli/options.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/registry.hpp"
|
||||
#include "core/version.hpp"
|
||||
#include "gui/window.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -375,6 +376,45 @@ std::vector<std::string> recentEntries(Gtk::Window& window) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
// The version and the changelog, each a menu entry away.
|
||||
void testHelpWindows(Gtk::Window& window) {
|
||||
std::cout << "changelog and about\n";
|
||||
auto toplevel = [](const std::string& title) -> Gtk::Window* {
|
||||
for (auto* w : Gtk::Window::list_toplevels()) {
|
||||
if (w->get_title() == title && w->get_visible()) return w;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
window.activate_action("win.show-changelog");
|
||||
pump(200);
|
||||
auto* changelog = toplevel("Changelog");
|
||||
check(changelog != nullptr, "Changelog opens a window");
|
||||
auto* text = changelog != nullptr ? named<Gtk::TextView>(*changelog, "changelog") : nullptr;
|
||||
const std::string shown = text != nullptr ? std::string(text->get_buffer()->get_text()) : "";
|
||||
check(shown.find(std::string(effector::kVersion) + " — ") != std::string::npos,
|
||||
"listing this version as a release");
|
||||
check(shown.find("[") == std::string::npos && shown.find("`") == std::string::npos &&
|
||||
shown.find("### ") == std::string::npos,
|
||||
"with the Markdown laid out rather than shown");
|
||||
if (changelog != nullptr) changelog->close();
|
||||
pump(50);
|
||||
check(toplevel("Changelog") == nullptr, "and closes");
|
||||
|
||||
window.activate_action("win.about");
|
||||
pump(200);
|
||||
Gtk::AboutDialog* about = nullptr;
|
||||
for (auto* w : Gtk::Window::list_toplevels()) {
|
||||
if (auto* dialog = dynamic_cast<Gtk::AboutDialog*>(w); dialog != nullptr && dialog->get_visible()) {
|
||||
about = dialog;
|
||||
}
|
||||
}
|
||||
check(about != nullptr && about->get_version() == std::string(effector::kVersion),
|
||||
"About Effector shows the version");
|
||||
if (about != nullptr) about->close();
|
||||
pump(50);
|
||||
}
|
||||
|
||||
void testRecentProjects(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||
std::cout << "recent projects\n";
|
||||
check(recentEntries(window) == std::vector<std::string>{"No Recent Projects"},
|
||||
@@ -425,6 +465,7 @@ int main(int argc, char** argv) {
|
||||
window->present();
|
||||
pump(300);
|
||||
runTests(*window);
|
||||
testHelpWindows(*window);
|
||||
testRecentProjects(*window, dataDir / "projects");
|
||||
app->remove_window(*window);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user