quit command.

main
bog 2024-01-10 18:32:26 +01:00
parent 69b0f500b7
commit 8d7cd962eb
24 changed files with 779 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*~*
*\#*
.cache
.ccls-cache
build

50
CMakeLists.txt Normal file
View File

@ -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 $<$<CONFIG:Debug>: -Wall -Wextra -g>)
target_link_libraries(pixdraw PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::OpenGLWidgets
$<TARGET_OBJECTS:pixlib>
)

16
Makefile Normal file
View File

@ -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 \

25
src/Presenter.cpp Normal file
View File

@ -0,0 +1,25 @@
#include "Presenter.hpp"
#include "cmd/pix_cmds.hpp"
namespace pix
{
/*explicit*/ Presenter::Presenter()
: m_window { std::make_unique<Window>(*this) }
{
m_listener.bind("C-c C-c", std::make_shared<QuitCmd>());
}
/*virtual*/ Presenter::~Presenter()
{
}
void Presenter::start()
{
m_window->show();
}
void Presenter::on_key_pressed(KeyMod const& km)
{
m_listener.update(km);
}
}

25
src/Presenter.hpp Normal file
View File

@ -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<Window> m_window;
ShortcutListener m_listener;
};
}
#endif

13
src/cmd/Command.cpp Normal file
View File

@ -0,0 +1,13 @@
#include "Command.hpp"
namespace pix
{
/*explicit*/ Command::Command(std::string const& name)
: m_name { name }
{
}
/*virtual*/ Command::~Command()
{
}
}

24
src/cmd/Command.hpp Normal file
View File

@ -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

View File

@ -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<Command> 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();
}
}
}
}

View File

@ -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<Command> cmd;
Shortcut shortcut;
size_t progress = 0;
};
class ShortcutListener
{
public:
explicit ShortcutListener();
virtual ~ShortcutListener();
void bind(std::string const& shortcut_repr, std::shared_ptr<Command> cmd);
void update(KeyMod const& keymod);
private:
std::vector<Binding> m_bindings;
};
}
#endif

23
src/cmd/pix_cmds.hpp Normal file
View File

@ -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

25
src/commons.hpp Normal file
View File

@ -0,0 +1,25 @@
#ifndef pix_COMMONS_HPP
#define pix_COMMONS_HPP
#include <vector>
#include <optional>
#include <memory>
#include <stdexcept>
#include <iostream>
#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

52
src/gui/Window.cpp Normal file
View File

@ -0,0 +1,52 @@
#include <QKeyEvent>
#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});
}
}

31
src/gui/Window.hpp Normal file
View File

@ -0,0 +1,31 @@
#ifndef pix_WINDOW_HPP
#define pix_WINDOW_HPP
#include <QMainWindow>
#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

33
src/gui/window.ui Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>599</height>
</rect>
</property>
<property name="windowTitle">
<string>Pix Draw Studio</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout"/>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>23</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

43
src/keys/KeyMod.cpp Normal file
View File

@ -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;
}
}

34
src/keys/KeyMod.hpp Normal file
View File

@ -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

65
src/keys/Shortcut.cpp Normal file
View File

@ -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()
{
}
}

23
src/keys/Shortcut.hpp Normal file
View File

@ -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<KeyMod> m_keymods;
};
}
#endif

13
src/main.cpp Normal file
View File

@ -0,0 +1,13 @@
#include <iostream>
#include <QApplication>
#include "Presenter.hpp"
int main(int argc, char** argv)
{
QApplication app {argc, argv};
pix::Presenter presenter;
presenter.start();
return app.exec();
}

17
tests/CMakeLists.txt Normal file
View File

@ -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
$<TARGET_OBJECTS:pixlib>
)

42
tests/KeyMod.cpp Normal file
View File

@ -0,0 +1,42 @@
#include <catch2/catch.hpp>
#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");
}
}

27
tests/Shortcut.cpp Normal file
View File

@ -0,0 +1,27 @@
#include <catch2/catch.hpp>
#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;
}

109
tests/ShortcutListener.cpp Normal file
View File

@ -0,0 +1,109 @@
#include <catch2/catch.hpp>
#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<CmdMock>();
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<CmdMock>();
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<CmdMock>();
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<CmdMock>();
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);
}

2
tests/main.cpp Normal file
View File

@ -0,0 +1,2 @@
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>