From 8d7cd962ebde3a6ab1b36cf417312a7ccaa41636 Mon Sep 17 00:00:00 2001 From: bog Date: Wed, 10 Jan 2024 18:32:26 +0100 Subject: [PATCH] :sparkles: quit command. --- .gitignore | 5 ++ CMakeLists.txt | 50 ++++++++++++++++ Makefile | 16 +++++ src/Presenter.cpp | 25 ++++++++ src/Presenter.hpp | 25 ++++++++ src/cmd/Command.cpp | 13 +++++ src/cmd/Command.hpp | 24 ++++++++ src/cmd/ShortcutListener.cpp | 52 +++++++++++++++++ src/cmd/ShortcutListener.hpp | 30 ++++++++++ src/cmd/pix_cmds.hpp | 23 ++++++++ src/commons.hpp | 25 ++++++++ src/gui/Window.cpp | 52 +++++++++++++++++ src/gui/Window.hpp | 31 ++++++++++ src/gui/window.ui | 33 +++++++++++ src/keys/KeyMod.cpp | 43 ++++++++++++++ src/keys/KeyMod.hpp | 34 +++++++++++ src/keys/Shortcut.cpp | 65 +++++++++++++++++++++ src/keys/Shortcut.hpp | 23 ++++++++ src/main.cpp | 13 +++++ tests/CMakeLists.txt | 17 ++++++ tests/KeyMod.cpp | 42 ++++++++++++++ tests/Shortcut.cpp | 27 +++++++++ tests/ShortcutListener.cpp | 109 +++++++++++++++++++++++++++++++++++ tests/main.cpp | 2 + 24 files changed, 779 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 src/Presenter.cpp create mode 100644 src/Presenter.hpp create mode 100644 src/cmd/Command.cpp create mode 100644 src/cmd/Command.hpp create mode 100644 src/cmd/ShortcutListener.cpp create mode 100644 src/cmd/ShortcutListener.hpp create mode 100644 src/cmd/pix_cmds.hpp create mode 100644 src/commons.hpp create mode 100644 src/gui/Window.cpp create mode 100644 src/gui/Window.hpp create mode 100644 src/gui/window.ui create mode 100644 src/keys/KeyMod.cpp create mode 100644 src/keys/KeyMod.hpp create mode 100644 src/keys/Shortcut.cpp create mode 100644 src/keys/Shortcut.hpp create mode 100644 src/main.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/KeyMod.cpp create mode 100644 tests/Shortcut.cpp create mode 100644 tests/ShortcutListener.cpp create mode 100644 tests/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..441682b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~* +*\#* +.cache +.ccls-cache +build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e54ca34 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.2) + +add_subdirectory(tests) + +project(pix-draw-studio + VERSION 0.0.0 + LANGUAGES CXX +) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUI ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGLWidgets) +qt_standard_project_setup() + +add_library(pixlib OBJECT + # keys + src/keys/KeyMod.cpp + src/keys/Shortcut.cpp + + # cmd + src/cmd/Command.cpp + src/cmd/ShortcutListener.cpp +) + +qt_add_executable(pixdraw + # entrypoint + src/main.cpp + src/Presenter.cpp + + # gui + src/gui/Window.hpp + src/gui/Window.cpp + src/gui/window.ui +) + +install(TARGETS pixdraw) + +target_compile_options(pixdraw + PRIVATE $<$: -Wall -Wextra -g>) + +target_link_libraries(pixdraw PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::OpenGLWidgets + $ +) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f14a633 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: build test + +build: + cmake -B build -DCMAKE_BUILD_TYPE=Debug + cmake --build build + +test: build + build/tests/pix-test + +install: test + cmake --install build + +check: + @cppcheck --enable=all language=c++ -q src tests \ + --suppress=missingInclude \ + --suppress=unusedFunction \ diff --git a/src/Presenter.cpp b/src/Presenter.cpp new file mode 100644 index 0000000..1cd51ce --- /dev/null +++ b/src/Presenter.cpp @@ -0,0 +1,25 @@ +#include "Presenter.hpp" +#include "cmd/pix_cmds.hpp" + +namespace pix +{ + /*explicit*/ Presenter::Presenter() + : m_window { std::make_unique(*this) } + { + m_listener.bind("C-c C-c", std::make_shared()); + } + + /*virtual*/ Presenter::~Presenter() + { + } + + void Presenter::start() + { + m_window->show(); + } + + void Presenter::on_key_pressed(KeyMod const& km) + { + m_listener.update(km); + } +} diff --git a/src/Presenter.hpp b/src/Presenter.hpp new file mode 100644 index 0000000..af01606 --- /dev/null +++ b/src/Presenter.hpp @@ -0,0 +1,25 @@ +#ifndef pix_PRESENTER_HPP +#define pix_PRESENTER_HPP + +#include "cmd/ShortcutListener.hpp" +#include "commons.hpp" +#include "gui/Window.hpp" + +namespace pix +{ + class Presenter + { + public: + explicit Presenter(); + virtual ~Presenter(); + + void start(); + void on_key_pressed(KeyMod const& km); + + private: + std::unique_ptr m_window; + ShortcutListener m_listener; + }; +} + +#endif diff --git a/src/cmd/Command.cpp b/src/cmd/Command.cpp new file mode 100644 index 0000000..4dd71e8 --- /dev/null +++ b/src/cmd/Command.cpp @@ -0,0 +1,13 @@ +#include "Command.hpp" + +namespace pix +{ + /*explicit*/ Command::Command(std::string const& name) + : m_name { name } + { + } + + /*virtual*/ Command::~Command() + { + } +} diff --git a/src/cmd/Command.hpp b/src/cmd/Command.hpp new file mode 100644 index 0000000..bc157da --- /dev/null +++ b/src/cmd/Command.hpp @@ -0,0 +1,24 @@ +#ifndef pix_COMMAND_HPP +#define pix_COMMAND_HPP + +#include "../commons.hpp" + +namespace pix +{ + class Command + { + public: + explicit Command(std::string const& name); + virtual ~Command(); + + std::string name() const { return m_name; } + + virtual void execute() = 0; + virtual void undo() {} + + private: + std::string m_name; + }; +} + +#endif diff --git a/src/cmd/ShortcutListener.cpp b/src/cmd/ShortcutListener.cpp new file mode 100644 index 0000000..deacdb0 --- /dev/null +++ b/src/cmd/ShortcutListener.cpp @@ -0,0 +1,52 @@ +#include "ShortcutListener.hpp" + +namespace pix +{ + /*explicit*/ ShortcutListener::ShortcutListener() + { + } + + /*virtual*/ ShortcutListener::~ShortcutListener() + { + } + + void ShortcutListener::bind(std::string const& shortcut_repr, + std::shared_ptr cmd) + { + Binding binding; + binding.cmd = cmd; + binding.shortcut = Shortcut {shortcut_repr}; + + m_bindings.push_back(binding); + } + + void ShortcutListener::update(KeyMod const& keymod) + { + for (auto& binding: m_bindings) + { + KeyMod current_km = binding.shortcut.get(binding.progress); + + if (current_km.equals(keymod)) + { + binding.progress++; + } + else + { + binding.progress = 0; + + current_km = binding.shortcut.get(binding.progress); + + if (current_km.equals(keymod)) + { + binding.progress++; + } + } + + if (binding.progress >= binding.shortcut.size()) + { + binding.progress = 0; + binding.cmd->execute(); + } + } + } +} diff --git a/src/cmd/ShortcutListener.hpp b/src/cmd/ShortcutListener.hpp new file mode 100644 index 0000000..f234423 --- /dev/null +++ b/src/cmd/ShortcutListener.hpp @@ -0,0 +1,30 @@ +#ifndef pix_SHORTCUTLISTENER_HPP +#define pix_SHORTCUTLISTENER_HPP + +#include "../keys/Shortcut.hpp" +#include "Command.hpp" + +namespace pix +{ + struct Binding { + std::shared_ptr cmd; + Shortcut shortcut; + size_t progress = 0; + }; + + class ShortcutListener + { + public: + explicit ShortcutListener(); + virtual ~ShortcutListener(); + + + void bind(std::string const& shortcut_repr, std::shared_ptr cmd); + void update(KeyMod const& keymod); + + private: + std::vector m_bindings; + }; +} + +#endif diff --git a/src/cmd/pix_cmds.hpp b/src/cmd/pix_cmds.hpp new file mode 100644 index 0000000..0206651 --- /dev/null +++ b/src/cmd/pix_cmds.hpp @@ -0,0 +1,23 @@ +#ifndef pix_PIX_CMDS_HPP +#define pix_PIX_CMDS_HPP + +#include "../commons.hpp" +#include "Command.hpp" + +namespace pix +{ + struct QuitCmd: public Command + { + explicit QuitCmd() + : Command("Quit") + { + } + + void execute() override + { + exit(0); + } + }; +} + +#endif diff --git a/src/commons.hpp b/src/commons.hpp new file mode 100644 index 0000000..bc7eefb --- /dev/null +++ b/src/commons.hpp @@ -0,0 +1,25 @@ +#ifndef pix_COMMONS_HPP +#define pix_COMMONS_HPP + +#include +#include +#include +#include +#include + +#define PIX_ENUM_ID(X) X +#define PIX_ENUM_STR(X) #X +#define PIX_ENUM(PREFIX, TYPE) \ + enum PREFIX { TYPE(PIX_ENUM_ID) }; \ + constexpr char const* PREFIX ## Str [] { TYPE(PIX_ENUM_STR) } + + +#define PIX_ERROR(NAME) \ + struct NAME : public std::runtime_error { \ + explicit NAME(std::string const& what) \ + : std::runtime_error {what} \ + { \ + } \ + } + +#endif diff --git a/src/gui/Window.cpp b/src/gui/Window.cpp new file mode 100644 index 0000000..58b74b1 --- /dev/null +++ b/src/gui/Window.cpp @@ -0,0 +1,52 @@ +#include +#include "Window.hpp" +#include "../Presenter.hpp" +#include "qnamespace.h" + +namespace pix +{ + /*explicit*/ Window::Window(Presenter& presenter, QWidget* parent) + : QMainWindow(parent) + , m_presenter { presenter } + { + m_ui.setupUi(this); + show(); + } + + /*virtual*/ Window::~Window() + { + } + + void Window::keyPressEvent(QKeyEvent* event) /*override*/ + { + auto combination = event->keyCombination(); + + if (event->key() == Qt::Key_Control + || event->key() == Qt::Key_Alt + || event->key() == Qt::Key_Shift) + { + return; + } + + std::string key(1, 'a' + (combination.key() - Qt::Key_A)); + + int mods = 0; + + if ((combination.keyboardModifiers() & Qt::ControlModifier) != 0) + { + mods |= PIX_MOD(PIX_CTRL); + } + + if ((combination.keyboardModifiers() & Qt::ShiftModifier) != 0) + { + mods |= PIX_MOD(PIX_SHIFT); + } + + if ((combination.keyboardModifiers() & Qt::AltModifier) != 0) + { + mods |= PIX_MOD(PIX_ALT); + } + + m_presenter.on_key_pressed(KeyMod {key, mods}); + } +} diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp new file mode 100644 index 0000000..4668642 --- /dev/null +++ b/src/gui/Window.hpp @@ -0,0 +1,31 @@ +#ifndef pix_WINDOW_HPP +#define pix_WINDOW_HPP + +#include +#include "ui_window.h" + +namespace Ui +{ + class MainWindow; +}; + +namespace pix +{ + class Presenter; + + class Window: public QMainWindow + { + Q_OBJECT + public: + explicit Window(Presenter& presenter, QWidget* parent=nullptr); + virtual ~Window(); + + void keyPressEvent(QKeyEvent* event) override; + + private: + Presenter& m_presenter; + Ui::MainWindow m_ui; + }; +} + +#endif diff --git a/src/gui/window.ui b/src/gui/window.ui new file mode 100644 index 0000000..edadf55 --- /dev/null +++ b/src/gui/window.ui @@ -0,0 +1,33 @@ + + + MainWindow + + + + 0 + 0 + 800 + 599 + + + + Pix Draw Studio + + + + + + + + 0 + 0 + 800 + 23 + + + + + + + + diff --git a/src/keys/KeyMod.cpp b/src/keys/KeyMod.cpp new file mode 100644 index 0000000..34126c3 --- /dev/null +++ b/src/keys/KeyMod.cpp @@ -0,0 +1,43 @@ +#include "KeyMod.hpp" + +namespace pix +{ + /*explicit*/ KeyMod::KeyMod(std::string const& key, int mods) + : m_key { key } + , m_mods { mods } + { + } + + /*virtual*/ KeyMod::~KeyMod() + { + } + + std::string KeyMod::string() const + { + std::string res; + + if (PIX_HAS_MOD(m_mods, PIX_CTRL)) + { + res += "C-"; + } + + if (PIX_HAS_MOD(m_mods, PIX_ALT)) + { + res += "A-"; + } + + if (PIX_HAS_MOD(m_mods, PIX_SHIFT)) + { + res += "S-"; + } + + res += m_key; + + return res; + } + + bool KeyMod::equals(KeyMod const& keymod) const + { + return m_key == keymod.m_key && m_mods == keymod.m_mods; + } +} diff --git a/src/keys/KeyMod.hpp b/src/keys/KeyMod.hpp new file mode 100644 index 0000000..af8a7f6 --- /dev/null +++ b/src/keys/KeyMod.hpp @@ -0,0 +1,34 @@ +#ifndef pds_KEYMOD_HPP +#define pds_KEYMOD_HPP + +#include "../commons.hpp" + +#define PIX_KEYMOD_TYPES(G) \ + G(PIX_NONE), \ + G(PIX_CTRL), \ + G(PIX_ALT), \ + G(PIX_SHIFT) + +#define PIX_MOD(MOD) (1 << MOD) +#define PIX_HAS_MOD(FLAGS, MOD) ( (FLAGS & PIX_MOD(MOD)) != 0 ) + +namespace pix +{ + PIX_ENUM(KeyModType, PIX_KEYMOD_TYPES); + + class KeyMod + { + public: + explicit KeyMod(std::string const& key, int mods=0); + virtual ~KeyMod(); + + std::string string() const; + bool equals(KeyMod const& keymod) const; + + private: + std::string m_key; + int m_mods; + }; +} + +#endif diff --git a/src/keys/Shortcut.cpp b/src/keys/Shortcut.cpp new file mode 100644 index 0000000..4636a60 --- /dev/null +++ b/src/keys/Shortcut.cpp @@ -0,0 +1,65 @@ +#include "Shortcut.hpp" + +namespace pix +{ + /*explicit*/ Shortcut::Shortcut() + { + } + + /*explicit*/ Shortcut::Shortcut(std::string const& repr) + { + std::string buffer; + int mods = 0; + + size_t i = 0; + + while (i < repr.size()) + { + char c = repr.at(i); + + if (std::isspace(c)) + { + if (buffer.empty() == false) + { + m_keymods.push_back(KeyMod(buffer, mods)); + buffer.clear(); + mods = 0; + } + i++; + } + else if (i + 1 < repr.size() && repr.at(i + 1) == '-') + { + if (c == 'C') + { + mods |= PIX_MOD(PIX_CTRL); + } + else if (c == 'A') + { + mods |= PIX_MOD(PIX_ALT); + } + else if (c == 'S') + { + mods |= PIX_MOD(PIX_SHIFT); + } + + i += 2; + } + else + { + buffer += c; + i++; + } + } + + if (buffer.empty() == false) + { + m_keymods.push_back(KeyMod(buffer, mods)); + buffer.clear(); + } + + } + + /*virtual*/ Shortcut::~Shortcut() + { + } +} diff --git a/src/keys/Shortcut.hpp b/src/keys/Shortcut.hpp new file mode 100644 index 0000000..260732a --- /dev/null +++ b/src/keys/Shortcut.hpp @@ -0,0 +1,23 @@ +#ifndef pix_SHORTCUT_HPP +#define pix_SHORTCUT_HPP + +#include "KeyMod.hpp" + +namespace pix +{ + class Shortcut + { + public: + explicit Shortcut(); + explicit Shortcut(std::string const& repr); + virtual ~Shortcut(); + + size_t size() const { return m_keymods.size(); } + KeyMod const& get(size_t index) const { return m_keymods.at(index); } + + private: + std::vector m_keymods; + }; +} + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..eb7aaa1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "Presenter.hpp" + +int main(int argc, char** argv) +{ + QApplication app {argc, argv}; + + pix::Presenter presenter; + presenter.start(); + + return app.exec(); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..b352d01 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.2) + +project(pix-draw-studio-tests) +find_package(Catch2 REQUIRED) + +add_executable(pix-test + main.cpp + + # keys + KeyMod.cpp + Shortcut.cpp + ShortcutListener.cpp +) + +target_link_libraries(pix-test + $ +) diff --git a/tests/KeyMod.cpp b/tests/KeyMod.cpp new file mode 100644 index 0000000..65b1a67 --- /dev/null +++ b/tests/KeyMod.cpp @@ -0,0 +1,42 @@ +#include +#include "../src/keys/KeyMod.hpp" + +class KeyModTest +{ +public: + explicit KeyModTest() {} + virtual ~KeyModTest() {} + +protected: +}; + +TEST_CASE_METHOD(KeyModTest, "KeyMod_to_string") +{ + SECTION("simple") + { + pix::KeyMod km {"a"}; + REQUIRE(km.string() == "a"); + } + + SECTION("one modifier") + { + pix::KeyMod km {"a", PIX_MOD(pix::PIX_CTRL)}; + REQUIRE(km.string() == "C-a"); + } + + SECTION("two modifiers") + { + pix::KeyMod km {"z", + PIX_MOD(pix::PIX_CTRL) + | PIX_MOD(pix::PIX_ALT)}; + REQUIRE(km.string() == "C-A-z"); + } + + SECTION("two modifiers reversed") + { + pix::KeyMod km {"k", + PIX_MOD(pix::PIX_SHIFT) + | PIX_MOD(pix::PIX_CTRL)}; + REQUIRE(km.string() == "C-S-k"); + } +} diff --git a/tests/Shortcut.cpp b/tests/Shortcut.cpp new file mode 100644 index 0000000..573929c --- /dev/null +++ b/tests/Shortcut.cpp @@ -0,0 +1,27 @@ +#include +#include "../src/keys/Shortcut.hpp" + +class ShortcutTest +{ +public: + explicit ShortcutTest() {} + virtual ~ShortcutTest() {} + +protected: +}; + +TEST_CASE_METHOD(ShortcutTest, "Shortcut_from_string") +{ + pix::Shortcut sc {"C-a A-b c C-S-s"}; + + REQUIRE(sc.size() == 4); + REQUIRE(sc.get(0).string() == "C-a"); + REQUIRE(sc.get(1).string() == "A-b"); + REQUIRE(sc.get(2).string() == "c"); + REQUIRE(sc.get(3).string() == "C-S-s"); +} + +TEST_CASE_METHOD(ShortcutTest, "Shortcut_to_string") +{ + pix::Shortcut sc; +} diff --git a/tests/ShortcutListener.cpp b/tests/ShortcutListener.cpp new file mode 100644 index 0000000..e77f560 --- /dev/null +++ b/tests/ShortcutListener.cpp @@ -0,0 +1,109 @@ +#include +#include "../src/cmd/ShortcutListener.hpp" +#include "../src/cmd/Command.hpp" +#include "../src/keys/KeyMod.hpp" + +class ShortcutListenerTest +{ +public: + explicit ShortcutListenerTest() {} + virtual ~ShortcutListenerTest() {} + +protected: + pix::ShortcutListener sl; +}; + +struct CmdMock: public pix::Command { + int count = 0; + + CmdMock(): pix::Command("Command Mock") + { + } + + void execute() override + { + count++; + } +}; + +TEST_CASE_METHOD(ShortcutListenerTest, "ShortcutListener_OneCommand") +{ + auto cmd = std::make_shared(); + + sl.bind("a", cmd); + + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"b"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"a"}); + REQUIRE(cmd->count == 1); +} + +TEST_CASE_METHOD(ShortcutListenerTest, "ShortcutListener_LongerShortcut") +{ + auto cmd_0 = std::make_shared(); + + sl.bind("a b C-c A-d", cmd_0); + + sl.update(pix::KeyMod {"a"}); + REQUIRE(cmd_0->count == 0); + + sl.update(pix::KeyMod {"b"}); + REQUIRE(cmd_0->count == 0); + + sl.update(pix::KeyMod {"c", PIX_MOD(pix::PIX_CTRL)}); + REQUIRE(cmd_0->count == 0); + + sl.update(pix::KeyMod {"d", PIX_MOD(pix::PIX_ALT)}); + REQUIRE(cmd_0->count == 1); + + sl.update(pix::KeyMod {"d", PIX_MOD(pix::PIX_CTRL)}); + REQUIRE(cmd_0->count == 1); +} + +TEST_CASE_METHOD(ShortcutListenerTest, "ShortcutListener_CommandFailed") +{ + auto cmd = std::make_shared(); + + sl.bind("b a t", cmd); + + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"b"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"a"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"b"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"a"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"t"}); + REQUIRE(cmd->count == 1); +} + +TEST_CASE_METHOD(ShortcutListenerTest, "ShortcutListener_FailedMiddle") +{ + auto cmd = std::make_shared(); + + sl.bind("b a t", cmd); + + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"b"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"a"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"x"}); + REQUIRE(cmd->count == 0); + + sl.update(pix::KeyMod {"t"}); + REQUIRE(cmd->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