diff --git a/Makefile b/Makefile index 9ea2b5e..8951fbc 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,15 @@ -.PHONY: build install +.PHONY: build tests install build: meson setup build meson compile -C build -install: build +tests: build + build/pixtool-tests + +install: tests meson install -C build + +check: + cppcheck --language=c++ --enable=all -q src \ + --suppress=missingInclude diff --git a/meson.build b/meson.build index c8bb258..f635e7c 100644 --- a/meson.build +++ b/meson.build @@ -26,20 +26,54 @@ qt_src = qt6.compile_moc( ] ) -executable( - 'pixtool', +pixtool = static_library( + 'pixtool-core', sources: [ - 'src/main.cpp', 'src/Presenter.cpp', # model - 'src/pixtool/PixTool.cpp', + 'src/model/PixTool.cpp', + 'src/model/Command.cpp', + 'src/model/Shortcut.cpp', + 'src/model/CommandRunner.cpp', + 'src/model/Observer.cpp', + 'src/model/Observable.cpp', # gui 'src/gui/Window.cpp', ] + qt_src, dependencies: [ + qt6_dep, + dependency('spdlog') + ] +) + +pixtool_dep = declare_dependency( + link_with: [pixtool], + include_directories: ['src'] +) + +executable( + 'pixtool', + sources: [ + 'src/main.cpp' + ], + dependencies: [ + pixtool_dep, qt6_dep ], install: true ) + +executable( + 'pixtool-tests', + sources: [ + 'tests/main.cpp', + 'tests/CommandRunner.cpp', + ], + dependencies: [ + dependency('catch2'), + pixtool_dep + ], + install: true +) diff --git a/src/Presenter.cpp b/src/Presenter.cpp index 935508a..16e4d8f 100644 --- a/src/Presenter.cpp +++ b/src/Presenter.cpp @@ -1,4 +1,5 @@ #include "Presenter.hpp" +#include "src/model/Event.hpp" namespace pt { @@ -11,4 +12,17 @@ namespace pt /*virtual*/ Presenter::~Presenter() { } + + void Presenter::update(model::Event& event) /*override*/ + { + if (event.type == model::EVENT_KEYPRESSED) + { + m_pixtool.update(event.key_pressed.keymod); + } + + if (event.type == model::EVENT_QUIT) + { + m_pixtool.quit(); + } + } } diff --git a/src/Presenter.hpp b/src/Presenter.hpp index b353db4..0847422 100644 --- a/src/Presenter.hpp +++ b/src/Presenter.hpp @@ -1,17 +1,21 @@ #ifndef pt_PRESENTER_HPP #define pt_PRESENTER_HPP -#include "pixtool/PixTool.hpp" +#include "model/PixTool.hpp" #include "gui/Window.hpp" +#include "model/Shortcut.hpp" +#include "model/Observer.hpp" +#include "src/model/Event.hpp" namespace pt { - class Presenter + class Presenter: public model::Observer { public: explicit Presenter(gui::Window& window, model::PixTool& pixtool); virtual ~Presenter(); + void update(model::Event& event) override; private: gui::Window& m_window; model::PixTool& m_pixtool; diff --git a/src/commons.hpp b/src/commons.hpp new file mode 100644 index 0000000..3cfcca3 --- /dev/null +++ b/src/commons.hpp @@ -0,0 +1,33 @@ +#ifndef pt_COMMONS_HPP +#define pt_COMMONS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "conf.hpp" + +#define PT_GEN_STR(X) #X +#define PT_GEN_ID(X) X + +#define PT_ENUM(PREFIX, ENUM) \ + enum PREFIX { ENUM(PT_GEN_ID) }; \ + constexpr char const* PREFIX ## Str [] = { ENUM(PT_GEN_STR) } + + +#define PT_ERROR(NAME) \ + struct NAME : public std::runtime_error \ + { \ + NAME ( std::string const& what ) \ + : std::runtime_error {what} \ + { \ + } \ + } + + +#endif diff --git a/src/gui/Window.cpp b/src/gui/Window.cpp index 11ca07a..dec03f1 100644 --- a/src/gui/Window.cpp +++ b/src/gui/Window.cpp @@ -1,5 +1,11 @@ +#include +#include "../model/Shortcut.hpp" #include "Window.hpp" #include "conf.hpp" +#include "qmainwindow.h" +#include "qnamespace.h" +#include "src/model/Event.hpp" +#include namespace pt { @@ -11,11 +17,56 @@ namespace pt setWindowTitle(QString::fromStdString("PixTool v" + PT_VERSION)); setMinimumSize(QSize {640, 480}); + QMenuBar* bar = new QMenuBar {this}; + + QMenu* file = new QMenu {"File"}; + bar->addMenu(file); + + QAction* quit = new QAction {"Quit"}; + file->addAction(quit); + + connect(quit, &QAction::triggered, [this](){ + model::Event ev {model::EVENT_QUIT}; + notify(ev); + }); + show(); } /*virtual*/ Window::~Window() { } + + void Window::keyPressEvent(QKeyEvent* event) /*override*/ + { + std::vector mods; + + auto mods_flag = event->modifiers(); + + if (mods_flag & Qt::ControlModifier) + { + mods.push_back(model::PT_CONTROL); + } + + if (mods_flag & Qt::AltModifier) + { + mods.push_back(model::PT_ALT); + } + + if (mods_flag & Qt::ShiftModifier) + { + mods.push_back(model::PT_SHIFT); + } + + char key = 'A' + (event->key() - Qt::Key_A); + + if (key >= 'A' && key <= 'Z') + { + model::KeyMod km {std::string(1, key), mods}; + model::Event e {model::EVENT_KEYPRESSED}; + e.key_pressed.keymod = km; + notify(e); + } + } } } diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp index 174eac5..264efd3 100644 --- a/src/gui/Window.hpp +++ b/src/gui/Window.hpp @@ -2,18 +2,23 @@ #define pt_gui_WINDOW_HPP #include +#include "../commons.hpp" +#include "src/model/Shortcut.hpp" +#include "../model/Observable.hpp" namespace pt { namespace gui { - class Window: public QMainWindow + class Window: public QMainWindow, public model::Observable { Q_OBJECT public: explicit Window(QWidget* parent=nullptr); virtual ~Window(); + void keyPressEvent(QKeyEvent* event) override; + private: }; } diff --git a/src/main.cpp b/src/main.cpp index d735ac5..3427cf7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,24 @@ #include #include -#include "pixtool/PixTool.hpp" +#include "model/PixTool.hpp" #include "gui/Window.hpp" #include "Presenter.hpp" +#include int main(int argc, char** argv) { + spdlog::set_level(spdlog::level::debug); + QApplication app {argc, argv}; pt::gui::Window window; pt::model::PixTool pixtool; - pt::Presenter presenter {window, pixtool}; + auto presenter = std::make_shared(window, pixtool); + window.add_observer(presenter); + + pixtool.ready(); return app.exec(); } diff --git a/src/model/Command.cpp b/src/model/Command.cpp new file mode 100644 index 0000000..7bb262c --- /dev/null +++ b/src/model/Command.cpp @@ -0,0 +1,21 @@ +#include "Command.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ Command::Command(std::string const& name) + : m_name { name } + { + } + + /*virtual*/ Command::~Command() + { + } + + /*virtual*/ void Command::execute(PixTool& /*pixtool*/) + { + } + + } +} diff --git a/src/model/Command.hpp b/src/model/Command.hpp new file mode 100644 index 0000000..f938311 --- /dev/null +++ b/src/model/Command.hpp @@ -0,0 +1,26 @@ +#ifndef pt_model_COMMAND_HPP +#define pt_model_COMMAND_HPP + +#include "../commons.hpp" +#include "PixTool.hpp" + +namespace pt +{ + namespace model + { + class Command + { + public: + explicit Command(std::string const& name); + virtual ~Command(); + + inline std::string name() const { return m_name; } + + virtual void execute(PixTool& pixtool); + private: + std::string m_name; + }; + } +} + +#endif diff --git a/src/model/CommandRunner.cpp b/src/model/CommandRunner.cpp new file mode 100644 index 0000000..5d49dda --- /dev/null +++ b/src/model/CommandRunner.cpp @@ -0,0 +1,62 @@ +#include +#include "CommandRunner.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ CommandRunner::CommandRunner(PixTool& pixtool) + : m_pixtool(pixtool) + { + } + + /*virtual*/ CommandRunner::~CommandRunner() + { + } + + void CommandRunner::bind(Shortcut const& shortcut, std::shared_ptr command) + { + m_entries.push_back(CommandEntry {shortcut, command}); + } + + void CommandRunner::update(KeyMod const& keymod) + { + for (auto& entry: m_entries) + { + if (entry.shortcut.key(entry.index) == keymod) + { + entry.index++; + + if (entry.index >= entry.shortcut.size()) + { + entry.index = 0; + execute(entry.command); + return; + } + } + else if (entry.shortcut.key(0) == keymod) + { + entry.index = 1; + + if (entry.index >= entry.shortcut.size()) + { + entry.index = 0; + execute(entry.command); + + return; + } + } + else + { + entry.index = 0; + } + } + } + + void CommandRunner::execute(std::shared_ptr command) + { + spdlog::debug("exec '" + command->name() + "'"); + command->execute(m_pixtool); + } + } +} diff --git a/src/model/CommandRunner.hpp b/src/model/CommandRunner.hpp new file mode 100644 index 0000000..f8a1edb --- /dev/null +++ b/src/model/CommandRunner.hpp @@ -0,0 +1,39 @@ +#ifndef pt_model_COMMANDRUNNER_HPP +#define pt_model_COMMANDRUNNER_HPP + +#include "../commons.hpp" +#include "Shortcut.hpp" +#include "Command.hpp" + +#include "PixTool.hpp" + +namespace pt +{ + namespace model + { + struct CommandEntry + { + Shortcut shortcut; + std::shared_ptr command; + size_t index = 0; + }; + + class CommandRunner + { + public: + explicit CommandRunner(PixTool& pixtool); + virtual ~CommandRunner(); + + void bind(Shortcut const& shortcut, std::shared_ptr command); + void update(KeyMod const& keymod); + + private: + PixTool& m_pixtool; + std::vector m_entries; + + void execute(std::shared_ptr command); + }; + } +} + +#endif diff --git a/src/model/Event.hpp b/src/model/Event.hpp new file mode 100644 index 0000000..9480cbd --- /dev/null +++ b/src/model/Event.hpp @@ -0,0 +1,32 @@ +#ifndef pt_model_EVENT_HPP +#define pt_model_EVENT_HPP + +#include "../commons.hpp" +#include "Shortcut.hpp" + +#define EVENT_TYPE(G) \ + G(EVENT_KEYPRESSED), \ + G(EVENT_QUIT) + +namespace pt +{ + namespace model + { + PT_ENUM(EventType, EVENT_TYPE); + + struct EventKeyPressed + { + KeyMod keymod {""}; + }; + + struct Event + { + explicit Event(EventType ty) : type { ty } {} + EventType type; + + EventKeyPressed key_pressed; + }; + } +} + +#endif diff --git a/src/model/Observable.cpp b/src/model/Observable.cpp new file mode 100644 index 0000000..1553cc9 --- /dev/null +++ b/src/model/Observable.cpp @@ -0,0 +1,31 @@ +#include "Observable.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ Observable::Observable() + { + } + + /*virtual*/ Observable::~Observable() + { + } + + void Observable::add_observer(std::shared_ptr observer) + { + m_observers.push_back(observer); + } + + void Observable::notify(Event& event) + { + for (auto observer: m_observers) + { + if (auto ob = observer.lock(); ob) + { + ob->update(event); + } + } + } + } +} diff --git a/src/model/Observable.hpp b/src/model/Observable.hpp new file mode 100644 index 0000000..73fe487 --- /dev/null +++ b/src/model/Observable.hpp @@ -0,0 +1,28 @@ +#ifndef pt_model_OBSERVABLE_HPP +#define pt_model_OBSERVABLE_HPP + +#include "../commons.hpp" +#include "Event.hpp" +#include "Observer.hpp" +#include + +namespace pt +{ + namespace model + { + class Observable + { + public: + explicit Observable(); + virtual ~Observable(); + + void add_observer(std::shared_ptr observer); + void notify(Event& event); + + private: + std::vector> m_observers; + }; + } +} + +#endif diff --git a/src/model/Observer.cpp b/src/model/Observer.cpp new file mode 100644 index 0000000..68416ae --- /dev/null +++ b/src/model/Observer.cpp @@ -0,0 +1,15 @@ +#include "Observer.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ Observer::Observer() + { + } + + /*virtual*/ Observer::~Observer() + { + } + } +} diff --git a/src/model/Observer.hpp b/src/model/Observer.hpp new file mode 100644 index 0000000..a219eea --- /dev/null +++ b/src/model/Observer.hpp @@ -0,0 +1,23 @@ +#ifndef pt_model_OBSERVER_HPP +#define pt_model_OBSERVER_HPP + +#include "../commons.hpp" +#include "Event.hpp" + +namespace pt +{ + namespace model + { + class Observer + { + public: + explicit Observer(); + virtual ~Observer(); + + virtual void update(Event& event) = 0; + private: + }; + } +} + +#endif diff --git a/src/model/PixTool.cpp b/src/model/PixTool.cpp new file mode 100644 index 0000000..a6fb139 --- /dev/null +++ b/src/model/PixTool.cpp @@ -0,0 +1,38 @@ +#include +#include "PixTool.hpp" +#include "CommandRunner.hpp" +#include "cmds/Quit.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ PixTool::PixTool() + : m_cmd_runner { std::make_unique(*this) } + { + m_cmd_runner->bind(Shortcut {{KeyMod {"X", {PT_CONTROL}}, + KeyMod {"C", {PT_CONTROL}}}}, + std::make_shared()); + } + + /*virtual*/ PixTool::~PixTool() + { + } + + void PixTool::ready() + { + spdlog::info("ready"); + } + + void PixTool::update(KeyMod const& km) + { + m_cmd_runner->update(km); + } + + void PixTool::quit() + { + spdlog::info("done"); + exit(0); + } + } +} diff --git a/src/pixtool/PixTool.hpp b/src/model/PixTool.hpp similarity index 51% rename from src/pixtool/PixTool.hpp rename to src/model/PixTool.hpp index ca4449c..9e7c98c 100644 --- a/src/pixtool/PixTool.hpp +++ b/src/model/PixTool.hpp @@ -1,17 +1,27 @@ #ifndef pt_model_PIXTOOL_HPP #define pt_model_PIXTOOL_HPP +#include "../commons.hpp" +#include "Shortcut.hpp" + namespace pt { namespace model { + class CommandRunner; + class PixTool { public: explicit PixTool(); virtual ~PixTool(); + void ready(); + void update(KeyMod const& km); + void quit(); + private: + std::unique_ptr m_cmd_runner; }; } } diff --git a/src/model/Shortcut.cpp b/src/model/Shortcut.cpp new file mode 100644 index 0000000..3a50fbe --- /dev/null +++ b/src/model/Shortcut.cpp @@ -0,0 +1,44 @@ +#include "Shortcut.hpp" + +namespace pt +{ + namespace model + { + /*explicit*/ Shortcut::Shortcut(std::vector const& keymods) + : m_keymods { keymods } + { + } + + /*virtual*/ Shortcut::~Shortcut() + { + } + + std::string Shortcut::string() const + { + std::stringstream ss; + + std::string sep; + + for (auto const& km: m_keymods) + { + ss << sep; + sep = " "; + + for (auto const& mod: km.mods) + { + ss << (ModTypeStr[mod] + strlen("PT_")) << "-"; + } + + ss << km.key; + } + + return ss.str(); + } + + KeyMod Shortcut::key(size_t index) const + { + assert(index < m_keymods.size()); + return m_keymods[index]; + } + } +} diff --git a/src/model/Shortcut.hpp b/src/model/Shortcut.hpp new file mode 100644 index 0000000..8985d74 --- /dev/null +++ b/src/model/Shortcut.hpp @@ -0,0 +1,66 @@ +#ifndef pt_model_SHORTCUT_HPP +#define pt_model_SHORTCUT_HPP + +#include "../commons.hpp" + +#define PT_KEYMOD(G) G(PT_NONE), G(PT_CONTROL), G(PT_ALT), G(PT_SHIFT) + +namespace pt +{ + namespace model + { + PT_ENUM(ModType, PT_KEYMOD); + + struct KeyMod + { + std::string key; + std::vector mods; + + explicit KeyMod(std::string const& k) + : KeyMod(k, {}) + { + } + + explicit KeyMod(std::string const& k, std::vector const& m) + : key { k } + , mods { m } + { + } + + inline bool operator==(KeyMod const& rhs) + { + if (key != rhs.key || mods.size() != rhs.mods.size()) + { + return false; + } + + for (size_t i=0; i const& keymods); + virtual ~Shortcut(); + + std::string string() const; + KeyMod key(size_t index) const; + + inline size_t size() const { return m_keymods.size(); } + + private: + std::vector m_keymods; + }; + } +} + +#endif diff --git a/src/model/cmds/Quit.hpp b/src/model/cmds/Quit.hpp new file mode 100644 index 0000000..dccaefb --- /dev/null +++ b/src/model/cmds/Quit.hpp @@ -0,0 +1,29 @@ +#ifndef pt_cmds_QUIT_HPP +#define pt_cmds_QUIT_HPP + +#include "../../commons.hpp" +#include "../Command.hpp" +#include "src/model/PixTool.hpp" + +namespace pt +{ + namespace cmds + { + class Quit: public model::Command + { + public: + explicit Quit(): + Command("quit") + { + } + + void execute(model::PixTool& pixtool) override + { + pixtool.quit(); + } + private: + }; + } +} + +#endif diff --git a/src/pixtool/PixTool.cpp b/src/pixtool/PixTool.cpp deleted file mode 100644 index 5655b6f..0000000 --- a/src/pixtool/PixTool.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "PixTool.hpp" - -namespace pt -{ - namespace model - { - /*explicit*/ PixTool::PixTool() - { - } - - /*virtual*/ PixTool::~PixTool() - { - } - } -} diff --git a/tests/CommandRunner.cpp b/tests/CommandRunner.cpp new file mode 100644 index 0000000..6b6a1e9 --- /dev/null +++ b/tests/CommandRunner.cpp @@ -0,0 +1,166 @@ +#include +#include "model/Command.hpp" +#include "model/CommandRunner.hpp" +#include "model/PixTool.hpp" +#include "model/Shortcut.hpp" + +using namespace pt::model; + +class CommandRunnerTest +{ +public: + explicit CommandRunnerTest() {} + virtual ~CommandRunnerTest() {} + +protected: + PixTool m_pixtool; + CommandRunner m_runner {m_pixtool}; +}; + +struct CommandMock: public Command +{ + int exec_count = 0; + + explicit CommandMock(): Command("mock") {} + + virtual void execute(PixTool&) override + { + exec_count++; + } +}; + +TEST_CASE_METHOD(CommandRunnerTest, "CommandRunner_one_key") +{ + + auto cmd = std::make_shared(); + + m_runner.bind(Shortcut {{KeyMod {"a"}}}, cmd); + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"b"}); + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"b"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 2); +} + + +TEST_CASE_METHOD(CommandRunnerTest, "CommandRunner_three_keys") +{ + + auto cmd = std::make_shared(); + + m_runner.bind(Shortcut {{KeyMod {"a"}, + KeyMod {"b", {PT_CONTROL}}, + KeyMod {"c", {PT_ALT}}}}, cmd); + + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"b", {PT_CONTROL}}); + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"c", {PT_ALT}}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"b"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"c", {PT_ALT}}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"b", {PT_CONTROL}}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"b", {PT_CONTROL}}); + REQUIRE(cmd->exec_count == 1); + + m_runner.update(KeyMod {"c", {PT_ALT}}); + REQUIRE(cmd->exec_count == 2); +} + +TEST_CASE_METHOD(CommandRunnerTest, "CommandRunner_two_commands") +{ + auto cmd0 = std::make_shared(); + auto cmd1 = std::make_shared(); + + m_runner.bind(Shortcut {{KeyMod {"a"}, + KeyMod {"b"}, + KeyMod {"c"}}}, cmd0); + + m_runner.bind(Shortcut {{KeyMod {"c"}, + KeyMod {"d"}, + KeyMod {"e"}}}, cmd1); + + m_runner.update(KeyMod {"a"}); + REQUIRE(cmd0->exec_count == 0); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"b"}); + REQUIRE(cmd0->exec_count == 0); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"c"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"d"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"e"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"c"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"d"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 0); + + m_runner.update(KeyMod {"e"}); + REQUIRE(cmd0->exec_count == 1); + REQUIRE(cmd1->exec_count == 1); +} + +TEST_CASE_METHOD(CommandRunnerTest, "CommandRunner_wrong_key") +{ + auto cmd = std::make_shared(); + + m_runner.bind(Shortcut {{KeyMod {"C", {PT_CONTROL}}, + KeyMod {"X", {PT_CONTROL}}}}, + cmd); + + REQUIRE(cmd->exec_count == 0); + + m_runner.update(KeyMod {"C", {PT_CONTROL}}); + m_runner.update(KeyMod {"V", {PT_CONTROL}}); + m_runner.update(KeyMod {"X", {PT_CONTROL}}); + + REQUIRE(cmd->exec_count == 0); +} diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..4ed06df --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,2 @@ +#define CATCH_CONFIG_MAIN +#include