From 537eafb100c138806b215936e69999a595bfb5c6 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Fri, 28 Nov 2025 00:00:00 +0000 Subject: [PATCH 1/8] feat: implement dynamic key buffer system --- src/common/utils/compression.cpp | 38 ++--- src/common/utils/compression.hpp | 38 ++--- src/common/utils/qt.cpp | 14 ++ src/common/utils/qt.hpp | 10 ++ src/controller/action.cpp | 24 +++ src/controller/action.hpp | 125 ++++++++++++++ src/controller/controller.cpp | 249 +++++++++++++++++++++++++--- src/controller/controller.hpp | 40 ++++- src/controller/keypress.hpp | 65 ++++++++ src/data-structures/uniquedeque.cpp | 0 src/data-structures/uniquedeque.hpp | 124 ++++++++++++++ src/tools/erasertool.cpp | 9 +- src/window/window.cpp | 1 + 13 files changed, 675 insertions(+), 62 deletions(-) create mode 100644 src/common/utils/qt.cpp create mode 100644 src/common/utils/qt.hpp create mode 100644 src/controller/action.cpp create mode 100644 src/controller/action.hpp create mode 100644 src/controller/keypress.hpp create mode 100644 src/data-structures/uniquedeque.cpp create mode 100644 src/data-structures/uniquedeque.hpp diff --git a/src/common/utils/compression.cpp b/src/common/utils/compression.cpp index 0fc0e5b..6c320bc 100644 --- a/src/common/utils/compression.cpp +++ b/src/common/utils/compression.cpp @@ -1,23 +1,23 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* Authors: -* 1. quarterstar - quarterstar@proton.me -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * Authors: + * 1. quarterstar - quarterstar@proton.me + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ // NOTE: qt headers MUST be placed before // kansi otherwise there will be a macro conflict diff --git a/src/common/utils/compression.hpp b/src/common/utils/compression.hpp index 24fb55a..24c1e08 100644 --- a/src/common/utils/compression.hpp +++ b/src/common/utils/compression.hpp @@ -1,23 +1,23 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* Authors: -* 1. quarterstar - quarterstar@proton.me -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * Authors: + * 1. quarterstar - quarterstar@proton.me + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #pragma once diff --git a/src/common/utils/qt.cpp b/src/common/utils/qt.cpp new file mode 100644 index 0000000..6e23c8e --- /dev/null +++ b/src/common/utils/qt.cpp @@ -0,0 +1,14 @@ +#include "qt.hpp" + +namespace Common::Utils::QtUtil { +uint64_t keyToken(const std::variant &v) { + uint64_t type = static_cast(v.index()); // 0 = Qt::Key, 1 = Qt::MouseButton + uint64_t val = 0; + if (std::holds_alternative(v)) { + val = static_cast(std::get(v)); + } else { + val = static_cast(std::get(v)); + } + return (type << 32) | (val & 0xffffffffULL); +} +} // namespace Common::Utils::QtUtil diff --git a/src/common/utils/qt.hpp b/src/common/utils/qt.hpp new file mode 100644 index 0000000..1c99b6f --- /dev/null +++ b/src/common/utils/qt.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include +#include + +namespace Common::Utils::QtUtil { +uint64_t keyToken(const std::variant &v); +} diff --git a/src/controller/action.cpp b/src/controller/action.cpp new file mode 100644 index 0000000..1c8ae87 --- /dev/null +++ b/src/controller/action.cpp @@ -0,0 +1,24 @@ +#include "action.hpp" + +void ToolTask::start(const TaskContext &ctx, const ActionState &state) { + m_toolBar.curTool().cleanup(); + m_canvas.setCursor(m_toolBar.tool(m_tool).cursor()); + m_toolBar.tool(m_tool).mousePressed(ctx.m_appContext); + + // This is needed so that the thing hovered + // over the first time a press is detected is immediately + // queued for deletion. + if (!state.m_prevHeld && m_tool == Tool::Eraser) { + m_toolBar.tool(m_tool).mouseMoved(ctx.m_appContext); + } +} + +void ToolTask::execute(const TaskContext &ctx, const ActionState &state) { + m_toolBar.tool(m_tool).mouseMoved(ctx.m_appContext); +} + +void ToolTask::stop(const TaskContext &ctx, const ActionState &state) { + m_toolBar.tool(m_tool).mouseReleased(ctx.m_appContext); + m_toolBar.tool(m_tool).cleanup(); + m_canvas.setCursor(m_toolBar.curTool().cursor()); +} diff --git a/src/controller/action.hpp b/src/controller/action.hpp new file mode 100644 index 0000000..0e1d60d --- /dev/null +++ b/src/controller/action.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include + +#include "../canvas/canvas.hpp" +#include "../common/utils/qt.hpp" +#include "../components/toolbar.hpp" +#include "../tools/tool.hpp" + +using namespace Common::Utils::QtUtil; + +struct ActionKey { + std::variant m_id; + bool m_mouse{false}; + size_t m_count{1}; +}; + +inline ActionKey key(Qt::MouseButton key, size_t count = 1) { + return ActionKey{.m_id = std::variant{key}, + .m_mouse = true, + .m_count = count}; +} + +inline ActionKey key(Qt::Key key, size_t count = 1) { + return ActionKey{.m_id = key, .m_mouse = false, .m_count = count}; +} + +struct ActionData { + std::vector m_keys; + bool m_holdRequired{false}; + int64_t m_maxGapMs{0}; +}; + +struct TaskContext { + ApplicationContext *m_appContext; +}; + +struct ActionState { + bool m_prevHeld{false}; +}; + +class IActionTask { +public: + virtual void start(const TaskContext &ctx, const ActionState &state) = 0; + virtual void execute(const TaskContext &ctx, const ActionState &state) = 0; + virtual void stop(const TaskContext &ctx, const ActionState &state) = 0; + + virtual ~IActionTask() = default; +}; + +class ToolTask : public IActionTask { +public: + ToolTask(ToolBar &toolBar, Canvas &canvas, Tool::Type tool) + : m_toolBar(toolBar), + m_canvas(canvas), + m_tool(tool) {} + + ToolBar &m_toolBar; + Canvas &m_canvas; + + Tool::Type m_tool; + + void start(const TaskContext &ctx, const ActionState &state) override; + void execute(const TaskContext &ctx, const ActionState &state) override; + void stop(const TaskContext &ctx, const ActionState &state) override; +}; + +struct ActionFacade { + ActionFacade(std::unique_ptr task, ActionData data) + : m_task(std::move(task)), + m_data(std::move(data)) {} + + std::unique_ptr m_task; + ActionData m_data; + ActionState m_state{}; + bool m_enabled{false}; + + inline void enable(const TaskContext &ctx) { + if (m_enabled) + return; + + if (m_task) + m_task->start(ctx, m_state); + + m_enabled = true; + } + + inline void execute(const TaskContext &ctx) { + if (!m_enabled || !m_task) + return; + + m_task->execute(ctx, m_state); + } + + inline void disable(const TaskContext &ctx) { + if (!m_enabled) + return; + + if (m_task) + m_task->stop(ctx, m_state); + + m_enabled = false; + } + + inline void toggle(TaskContext ctx) { m_enabled ? disable(ctx) : enable(ctx); } +}; + +inline std::vector getDefaultActions(ToolBar &toolbar, Canvas &canvas) { + std::vector result; + + result.push_back(ActionFacade{ + std::make_unique(toolbar, canvas, Tool::Move), + ActionData{{key(Qt::Key_Space), key(Qt::LeftButton)}, true, -2}, + }); + + result.push_back(ActionFacade{ + std::make_unique(toolbar, canvas, Tool::Eraser), + ActionData{{key(Qt::RightButton)}, true, -2}, + }); + + return result; +} diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index 50f2106..476a4be 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -2,6 +2,10 @@ * Drawy - A simple brainstorming tool with an infinite canvas * Copyright (C) 2025 - Prayag Jain * + * Authors: + * 1. Prayag Jain - prayagjain2@gmail.com + * 2. quarterstar - quarterstar@proton.me + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -20,10 +24,16 @@ #include #include +#include +#include +#include +#include +#include #include "../canvas/canvas.hpp" #include "../common/constants.hpp" #include "../common/renderitems.hpp" +#include "../common/utils/qt.hpp" #include "../components/toolbar.hpp" #include "../context/applicationcontext.hpp" #include "../context/renderingcontext.hpp" @@ -32,6 +42,8 @@ #include "../event/event.hpp" #include "../tools/tool.hpp" +using namespace Common::Utils::QtUtil; + Controller::Controller(QObject *parent) : QObject{parent} { m_context = ApplicationContext::instance(dynamic_cast(parent)); m_context->setContexts(); @@ -42,6 +54,12 @@ Controller::~Controller() { } void Controller::mousePressed(QMouseEvent *event) { + std::variant keyVar{static_cast(event->button())}; + KeyPress kp{keyVar, QDateTime::currentMSecsSinceEpoch()}; + m_keyBuffer.push_back(kp); + + this->anyPressed(event); + // No on really clicks in this corner (0, 0) and this solves a // bug on Hyprland where it would register a mouse press in this corner if (event->pos() == QPoint{0, 0}) @@ -49,7 +67,8 @@ void Controller::mousePressed(QMouseEvent *event) { qint64 lastTime{m_lastClickTime}; m_lastClickTime = QDateTime::currentMSecsSinceEpoch(); - if (m_lastClickTime - lastTime <= Common::doubleClickInterval && !m_mouseMoved) { + if (m_lastClickTime - lastTime <= Common::doubleClickInterval && !m_mouseMoved && + m_occupationKeyBuffer.empty()) { m_clickCount++; if (m_clickCount == 2) { @@ -71,21 +90,22 @@ void Controller::mousePressed(QMouseEvent *event) { contextEvent.setButton(event->button()); contextEvent.setModifiers(event->modifiers()); - if (event->button() == Qt::MiddleButton) { - m_movingWithMiddleClick = true; - toolBar.curTool().cleanup(); - toolBar.tool(Tool::Move).mousePressed(m_context); - return; + if (m_occupationKeyBuffer.empty()) { + toolBar.curTool().mousePressed(m_context); } - toolBar.curTool().mousePressed(m_context); - if (event->type() != QEvent::TabletPress) { contextEvent.setPressure(1.0); } + + m_prevKeyBuffer = m_keyBuffer; + m_lastMousePress = event->button(); } void Controller::mouseDoubleClick(QMouseEvent *event) { + if (!m_occupationKeyBuffer.empty()) + return; + Event &contextEvent{m_context->uiContext().event()}; ToolBar &toolBar{m_context->uiContext().toolBar()}; Canvas &canvas{m_context->renderingContext().canvas()}; @@ -98,6 +118,9 @@ void Controller::mouseDoubleClick(QMouseEvent *event) { } void Controller::mouseTripleClick(QMouseEvent *event) { + if (!m_occupationKeyBuffer.empty()) + return; + Event &contextEvent{m_context->uiContext().event()}; ToolBar &toolBar{m_context->uiContext().toolBar()}; Canvas &canvas{m_context->renderingContext().canvas()}; @@ -120,15 +143,23 @@ void Controller::mouseMoved(QMouseEvent *event) { contextEvent.setButton(event->button()); contextEvent.setModifiers(event->modifiers()); - if (m_movingWithMiddleClick) { - toolBar.tool(Tool::Move).mouseMoved(m_context); - return; + for (auto &action : m_actions) { + action.execute(m_taskCtx); } - toolBar.curTool().mouseMoved(m_context); + if (m_occupationKeyBuffer.empty()) { + toolBar.curTool().mouseMoved(m_context); + } } void Controller::mouseReleased(QMouseEvent *event) { + auto old_occupationKeyBuffer = m_occupationKeyBuffer; + + m_keyBuffer.erase_if( + [&](auto const &v) { return keyToken(v.m_key) == keyToken(event->button()); }); + + this->anyReleased(event); + Event &contextEvent{m_context->uiContext().event()}; ToolBar &toolBar{m_context->uiContext().toolBar()}; Canvas &canvas{m_context->renderingContext().canvas()}; @@ -137,14 +168,11 @@ void Controller::mouseReleased(QMouseEvent *event) { contextEvent.setButton(event->button()); contextEvent.setModifiers(event->modifiers()); - if (event->button() == Qt::MiddleButton) { - m_movingWithMiddleClick = false; - toolBar.tool(Tool::Move).mouseReleased(m_context); - canvas.setCursor(toolBar.curTool().cursor()); - return; + if (old_occupationKeyBuffer.empty()) { + toolBar.curTool().mouseReleased(m_context); } - toolBar.curTool().mouseReleased(m_context); + m_prevKeyBuffer = m_keyBuffer; } void Controller::tablet(QTabletEvent *event) { @@ -155,6 +183,12 @@ void Controller::tablet(QTabletEvent *event) { } void Controller::keyPressed(QKeyEvent *event) { + std::variant keyVar{static_cast(event->key())}; + KeyPress kp{keyVar, QDateTime::currentMSecsSinceEpoch()}; + m_keyBuffer.push_back(kp); + + this->anyPressed(event); + Event &contextEvent{m_context->uiContext().event()}; ToolBar &toolBar{m_context->uiContext().toolBar()}; @@ -162,10 +196,29 @@ void Controller::keyPressed(QKeyEvent *event) { contextEvent.setModifiers(event->modifiers()); contextEvent.setText(event->text()); - toolBar.curTool().keyPressed(m_context); + if (m_occupationKeyBuffer.empty()) { + toolBar.curTool().keyPressed(m_context); + } + + m_prevKeyBuffer = m_keyBuffer; + m_lastKeyPress = static_cast(event->key()); } void Controller::keyReleased(QKeyEvent *event) { + auto old_occupationKeyBuffer = m_occupationKeyBuffer; + + if (event->isAutoRepeat()) { + return; + } + + m_prevKeyBuffer = m_keyBuffer; + // Issue is caused here unless auto repeat events are ignored + m_keyBuffer.erase_if([&](auto const &v) { + return keyToken(v.m_key) == keyToken(static_cast(event->key())); + }); + + this->anyReleased(event); + Event &contextEvent{m_context->uiContext().event()}; ToolBar &toolBar{m_context->uiContext().toolBar()}; @@ -173,7 +226,50 @@ void Controller::keyReleased(QKeyEvent *event) { contextEvent.setModifiers(event->modifiers()); contextEvent.setText(event->text()); - toolBar.curTool().keyReleased(m_context); + if (old_occupationKeyBuffer.empty()) { + toolBar.curTool().keyReleased(m_context); + } +} + +void Controller::anyPressed(std::variant input) { + for (auto &action : m_actions) { + auto keysView = + action.m_data.m_keys | std::views::transform([](auto key) { return key.m_id; }); + std::vector> keys{keysView.begin(), keysView.end()}; + + bool hasAllMoveKeys = action.m_data.m_maxGapMs < 0 + ? this->satisfiesKeys(keys) + : this->satisfiesKeys(keys, std::nullopt); + + if (m_occupationKeyBuffer.empty() && hasAllMoveKeys) { + m_occupationKeyBuffer = m_keyBuffer; + action.enable(m_taskCtx); + } + + action.m_state.m_prevHeld = hasAllMoveKeys; + } +} + +void Controller::anyReleased(std::variant input) { + for (auto &action : m_actions) { + bool hasAllMoveKeys = + std::ranges::all_of(action.m_data.m_keys, [&](const auto &requiredKey) { + return std::ranges::any_of(m_keyBuffer, [&](const auto &pressedKey) { + return pressedKey.m_key == requiredKey.m_id; + }); + }); + + if (!hasAllMoveKeys) { + m_occupationKeyBuffer.clear(); + action.disable(m_taskCtx); + } + + action.m_state.m_prevHeld = hasAllMoveKeys; + } + + if (m_keyBuffer.empty()) { + m_occupationKeyBuffer.clear(); + } } void Controller::inputMethodInvoked(QInputMethodEvent *event) { @@ -201,3 +297,116 @@ void Controller::wheel(QWheelEvent *event) { m_context->renderingContext().markForRender(); m_context->renderingContext().markForUpdate(); } + +bool Controller::satisfiesKeys(std::vector> &required, + std::optional maxGapMs) { + using std::begin; + using std::end; + + // Fast path: ordered search in buffer (insertion order) + auto bufIt = m_keyBuffer.begin(); + for (auto const &reqKey : required) { + bufIt = std::find_if(bufIt, m_keyBuffer.end(), [&](const KeyPress &kp) { + return keyToken(kp.m_key) == keyToken(reqKey); + }); + + if (bufIt == m_keyBuffer.end()) { + goto try_timed; + } + + bufIt++; + } + + return true; // ordered sequence found + +try_timed: + if (!maxGapMs.has_value()) { + return false; + } + + // build required counts (respect multiplicity) + std::unordered_map reqCounts; + reqCounts.reserve(required.size() * 2); + for (auto const &r : required) + ++reqCounts[keyToken(r)]; + + // keep only events whose key is in required + struct Tiny { + uint64_t m_token; + qint64 m_timeMs; + }; + std::vector events; + events.reserve(m_keyBuffer.size()); + for (auto const &ev : m_keyBuffer) { + uint64_t t = keyToken(ev.m_key); + if (reqCounts.find(t) != reqCounts.end()) { + events.push_back({t, ev.m_timeMs}); + } + } + if (events.empty()) + return false; + + std::sort(events.begin(), events.end(), [](auto const &a, auto const &b) { + return a.m_timeMs < b.m_timeMs; + }); + + const auto n = static_cast(events.size()); + + for (size_t s = 0; s < n; s++) { + // quick skip: start event must be a required token (it is, by construction) + std::unordered_map have; + have.reserve(reqCounts.size()); + have[events[s].m_token] = 1; + qint64 prevTime = events[s].m_timeMs; + + // quick full-match check + bool full = true; + + for (auto const &p : reqCounts) { + int got = (have.find(p.first) != have.end()) ? have[p.first] : 0; + if (got < p.second) { + full = false; + break; + } + } + + if (full) + return true; + + for (size_t j = s + 1; j < n; j++) { + qint64 gap = events[j].m_timeMs - prevTime; + if (gap > maxGapMs.value()) + break; + + uint64_t tok = events[j].m_token; + int needed = reqCounts[tok]; + int haveNow = (have.find(tok) != have.end()) ? have[tok] : 0; + + if (haveNow < needed) { + have[tok] = haveNow + 1; + prevTime = events[j].m_timeMs; // advance the matched chain time + } + + // check full + bool done = true; + for (auto const &p : reqCounts) { + int g = (have.find(p.first) != have.end()) ? have[p.first] : 0; + if (g < p.second) { + done = false; + break; + } + } + if (done) + return true; + } + } + + return false; +} + +void Controller::initialize() { + ToolBar &toolBar{m_context->uiContext().toolBar()}; + Canvas &canvas{m_context->renderingContext().canvas()}; + m_actions = getDefaultActions(toolBar, canvas); + m_taskCtx = TaskContext{.m_appContext = m_context}; +} diff --git a/src/controller/controller.hpp b/src/controller/controller.hpp index 511314e..92aba6c 100644 --- a/src/controller/controller.hpp +++ b/src/controller/controller.hpp @@ -2,6 +2,10 @@ * Drawy - A simple brainstorming tool with an infinite canvas * Copyright (C) 2025 - Prayag Jain * + * Authors: + * 1. Prayag Jain - prayagjain2@gmail.com + * 2. quarterstar - quarterstar@proton.me + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -21,10 +25,20 @@ #include #include #include +#include +#include +#include "../data-structures/uniquedeque.hpp" #include "../tools/tool.hpp" +#include "action.hpp" +#include "keypress.hpp" + class ApplicationContext; +namespace { +static constexpr qint64 DEFAULT_KEY_INPUT_THRESHOLD_MS = 100; +} + class Controller : public QObject { Q_OBJECT @@ -33,6 +47,7 @@ class Controller : public QObject { ~Controller(); public slots: + void initialize(); void mousePressed(QMouseEvent *event); void mouseMoved(QMouseEvent *event); void mouseReleased(QMouseEvent *event); @@ -40,6 +55,8 @@ public slots: void mouseTripleClick(QMouseEvent *event); void keyPressed(QKeyEvent *event); void keyReleased(QKeyEvent *event); + void anyPressed(std::variant input); + void anyReleased(std::variant input); void inputMethodInvoked(QInputMethodEvent *event); void tablet(QTabletEvent *event); void wheel(QWheelEvent *event); @@ -49,8 +66,27 @@ public slots: ApplicationContext *m_context{}; qint64 m_lastTime{}; qint64 m_lastClickTime{}; - int m_clickCount{}; // for double/triple clicks + size_t m_clickCount{}; // for double/triple clicks bool m_mouseMoved{false}; - bool m_movingWithMiddleClick{false}; + + std::vector m_actions{}; + + UniqueDeque m_keyBuffer{}; + UniqueDeque m_prevKeyBuffer{}; + UniqueDeque m_occupationKeyBuffer{}; + + Qt::Key m_lastKeyPress{}; + Qt::MouseButton m_lastMousePress{}; + + // The key buffer system accepts a sequence of inputs if they are either + // in the same insertion order as the one they were defined as or the + // time difference between the key presses is negligible. For example, + // the user may intrinsically try to move around with Space + Left Click + // but press Left Click a few milliseconds before Space. In that case, the + // user's input will still be accepted. + bool satisfiesKeys(std::vector> &required, + std::optional maxGapMs = DEFAULT_KEY_INPUT_THRESHOLD_MS); + + TaskContext m_taskCtx; }; diff --git a/src/controller/keypress.hpp b/src/controller/keypress.hpp new file mode 100644 index 0000000..c99ee79 --- /dev/null +++ b/src/controller/keypress.hpp @@ -0,0 +1,65 @@ +/* + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * Authors: + * 1. quarterstar - quarterstar@proton.me + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +struct KeyPress { + std::variant m_key; + qint64 m_timeMs{}; + + KeyPress() noexcept = default; + KeyPress(std::variant k, qint64 t) noexcept + : m_key(std::move(k)), + m_timeMs(t) {} + + KeyPress(const KeyPress &) = default; + KeyPress(KeyPress &&) noexcept = default; + KeyPress &operator=(const KeyPress &) = default; + KeyPress &operator=(KeyPress &&) noexcept = default; + ~KeyPress() = default; + + // equality is used by UniqueDeque so we only + // compare the key + bool operator==(const KeyPress &o) const noexcept { return m_key == o.m_key; } +}; + +namespace std { +template <> +struct hash { + size_t operator()(KeyPress const &kp) const noexcept { + uint64_t type = static_cast(kp.m_key.index()); // 0 = variant index + uint64_t val = 0; + if (std::holds_alternative(kp.m_key)) { + val = static_cast(std::get(kp.m_key)); + } else { + val = static_cast(std::get(kp.m_key)); + } + uint64_t combined = (type << 32) | (val & 0xffffffffULL); + return std::hash{}(combined); + } +}; +} // namespace std diff --git a/src/data-structures/uniquedeque.cpp b/src/data-structures/uniquedeque.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/data-structures/uniquedeque.hpp b/src/data-structures/uniquedeque.hpp new file mode 100644 index 0000000..86ff1f8 --- /dev/null +++ b/src/data-structures/uniquedeque.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +template +class UniqueDeque { +private: + std::deque m_deque; + std::unordered_set m_set; + +public: + bool push_back(const T &value); + bool push_front(const T &value); + + T &operator[](size_t index); + const T &operator[](size_t index) const; + + size_t size() const; + + bool empty() const; + + auto begin(); + auto end(); + auto cbegin() const; + auto cend() const; + + bool erase(const T &value); + + void clear(); + + template + void erase_if(Predicate pred); +}; + +template +bool UniqueDeque::push_back(const T &value) { + if (m_set.insert(value).second) { + m_deque.push_back(value); + return true; + } + return false; +} + +template +bool UniqueDeque::push_front(const T &value) { + if (m_set.insert(value).second) { + m_deque.push_front(value); + return true; + } + return false; +} + +template +T &UniqueDeque::operator[](size_t index) { + return m_deque[index]; +} + +template +const T &UniqueDeque::operator[](size_t index) const { + return m_deque[index]; +} + +template +size_t UniqueDeque::size() const { + return m_deque.size(); +} + +template +bool UniqueDeque::empty() const { + return m_deque.empty(); +} + +template +auto UniqueDeque::begin() { + return m_deque.begin(); +} + +template +auto UniqueDeque::end() { + return m_deque.end(); +} + +template +auto UniqueDeque::cbegin() const { + return m_deque.cbegin(); +} + +template +auto UniqueDeque::cend() const { + return m_deque.cend(); +} + +template +bool UniqueDeque::erase(const T &value) { + if (m_set.erase(value)) { + auto it = std::find(m_deque.begin(), m_deque.end(), value); + if (it != m_deque.end()) { + m_deque.erase(it); + return true; + } + } + return false; +} + +template +void UniqueDeque::clear() { + m_deque.clear(); + m_set.clear(); +} + +template +template +void UniqueDeque::erase_if(Predicate pred) { + for (auto it = m_deque.begin(); it != m_deque.end();) { + if (pred(*it)) { + m_set.erase(*it); + it = m_deque.erase(it); + } else { + ++it; + } + } +} diff --git a/src/tools/erasertool.cpp b/src/tools/erasertool.cpp index 6b080a4..6466b3b 100644 --- a/src/tools/erasertool.cpp +++ b/src/tools/erasertool.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "../canvas/canvas.hpp" #include "../command/commandhistory.hpp" @@ -38,6 +39,10 @@ #include "../item/item.hpp" #include "../properties/widgets/propertymanager.hpp" +namespace { +static constexpr std::array BUTTONS = {Qt::LeftButton, Qt::RightButton}; +} + EraserTool::EraserTool() { m_cursor = QCursor(Qt::CrossCursor); @@ -47,7 +52,7 @@ EraserTool::EraserTool() { void EraserTool::mousePressed(ApplicationContext *context) { Event &event{context->uiContext().event()}; - if (event.button() == Qt::LeftButton) { + if (std::ranges::find(BUTTONS, event.button()) != BUTTONS.end()) { m_isErasing = true; } }; @@ -112,7 +117,7 @@ void EraserTool::mouseMoved(ApplicationContext *context) { void EraserTool::mouseReleased(ApplicationContext *context) { UIContext &uiContext{context->uiContext()}; - if (uiContext.event().button() == Qt::LeftButton) { + if (std::ranges::find(BUTTONS, uiContext.event().button())) { SpatialContext &spatialContext{context->spatialContext()}; CoordinateTransformer &transformer{spatialContext.coordinateTransformer()}; RenderingContext &renderingContext{context->renderingContext()}; diff --git a/src/window/window.cpp b/src/window/window.cpp index fdd9ef6..b0bd3e7 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -38,6 +38,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { BoardLayout *layout{new BoardLayout(this)}; Controller *controller{new Controller(this)}; + controller->initialize(); ApplicationContext *context{ApplicationContext::instance()}; RenderingContext &renderingContext{context->renderingContext()}; From 6be36caf3f61250da4763946cc96ce5084b060ef Mon Sep 17 00:00:00 2001 From: quarterstar Date: Fri, 28 Nov 2025 00:00:00 +0000 Subject: [PATCH 2/8] docs: add new shortcuts to readme --- README.md | 87 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index ee580e3..4325e9f 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ Drawy is a work-in-progress infinite whiteboard tool written in Qt/C++, which aims to be a native-desktop alternative to the amazing web-based Excalidraw. # Community -Join our Discord server to engage with our community and help shape the future of Drawy! - -Static Badge -# Installation +Join our Discord server to engage with our community and help shape the future of Drawy! + +Static Badge + +# Installation + Static Badge @@ -29,6 +31,7 @@ Join our Discord server to engage with our community and help shape the future o ## Compiling from Source + - Install `cmake` and `g++` - Install Qt 6.9 or above from [here](https://www.qt.io/download-qt-installer-oss) or using [aqtinstall](https://github.com/miurahr/aqtinstall) - Clone this repository: `git clone https://github.com/prayag2/drawy && cd drawy` @@ -37,64 +40,76 @@ Join our Discord server to engage with our community and help shape the future o - Run: `./build/drawy` # Keyboard Shortcuts -Future releases will allow you to change the keyboard shortcuts. For now they are hardcoded. Here's a list of all available keyboard shortcuts: -| Key Combination | Description | -|:---------------------------------------------------------------------------:|:-----------------:| -| Ctrl + Z | Undo | -| Ctrl + Y, Ctrl + Shift + Z | Redo | -| Ctrl + + | Zoom In | -| Ctrl + - | Zoom Out | -| Ctrl + G | Group selection | -| Ctrl + Shift + G | Ungroup selection | -| P, B | Freeform Tool | -| E | Eraser Tool | -| S | Selection Tool | -| R | Rectangle Tool | -| O | Ellipse Tool | -| L | Line Tool | -| A | Arrow Tool | -| M | Move Tool | -| T | Text Tool | -| Ctrl+A | Select All | -| Delete | Delete selection | -| Ctrl+S | Save | -| Ctrl+O | Open File | + +> [!NOTE] +> Future releases will allow you to change the keyboard shortcuts. + +| Key Combination | Description | +|:---------------------------------------------------------------------------------:|:--------------------------:| +| Ctrl + Z | Undo | +| Ctrl + Y, Ctrl + Shift + Z | Redo | +| Ctrl + + | Zoom In | +| Ctrl + - | Zoom Out | +| Ctrl + G | Group selection | +| Ctrl + Shift + G | Ungroup selection | +| P, B | Freeform Tool | +| E | Eraser Tool | +| S | Selection Tool | +| R | Rectangle Tool | +| O | Ellipse Tool | +| L | Line Tool | +| A | Arrow Tool | +| M | Move Tool | +| T | Text Tool | +| Ctrl+A | Select All | +| Delete | Delete selection | +| Ctrl+S | Save | +| Ctrl+O | Open File | +| Right Click (Hold) | Eraser Tool | +| Space + Left Click | Move Tool | # Contributing -Contributions are welcome. Please read the [contributing guide](CONTRIBUTING.md) before opening pull requests. + +Contributions are welcome. Please read the [Contributing Guide](CONTRIBUTING.md) before opening pull requests. # License + This project uses the GNU General Public License V3. # Support Me ♥ -If you liked this project, then please consider supporting me! - + +If you liked this project, then please consider supporting me! + Donate using Liberapay Buy Me A Coffee Done using PayPal # TODOs -Started: `2025-01-02 04:40PM` -Development is divided into phases. -The project will eventually be open sourced. However, I will work on the first few phases myself, to maximize learning. -The following is a list of features I'll be planning to add to it: + +Started: `2025-01-02 04:40PM` +Development is divided into phases. +The project will eventually be open sourced. However, I will work on the first few phases myself, to maximize learning. +The following is a list of features I'll be planning to add to it: ## Phase 1 (Basic Features) + - [x] A simple fixed size canvas to draw on using a black coloured stroke. - [x] Different shapes like rectangle, ellipse, arrow, line and stroke. - [x] An eraser to erase the strokes (deleting the strokes). - [x] Testing. ## Phase 2 + - [x] Refactor to try to adhere to SOLID principles and utilize useful design patterns - [x] A custom Qt layout for toolbar and properties bar - [x] Make canvas infinite and add ability to move the viewport - [x] Use an LRU cache based uniform grid to optimize moving the canvas around (now 100% faster!!) - [x] A properties bar to change the following properties: - [x] Colour of strokes - - [x] Stroke width - + - [x] Stroke width + ## Phase 3 + - [x] Buttons to zoom in/out - [x] Pressure senstivity for drawing tablets - [x] Selection tool to select items and do these actions: @@ -112,7 +127,7 @@ The following is a list of features I'll be planning to add to it: - [ ] Better freeform smoothing algorithms - [ ] Allow snapping - [ ] Ability to store preferences -- [ ] A "settings" page +- [ ] A "settings" page - [ ] Better widgets - [ ] Online collaboration From 6dd6ad46538441f9e061fee2250852924d672ae1 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Fri, 28 Nov 2025 00:00:00 +0000 Subject: [PATCH 3/8] chore: reformat code --- src/command/deselectcommand.cpp | 37 ++++++------- src/command/deselectcommand.hpp | 32 +++++------ src/command/groupcommand.cpp | 38 ++++++------- src/command/groupcommand.hpp | 32 +++++------ src/command/selectcommand.cpp | 37 ++++++------- src/command/selectcommand.hpp | 32 +++++------ src/command/ungroupcommand.cpp | 35 ++++++------ src/command/ungroupcommand.hpp | 32 +++++------ src/data-structures/quadtree.cpp | 4 +- src/data-structures/quadtree.hpp | 2 +- src/item/group.cpp | 9 ++-- src/item/group.hpp | 34 ++++++------ src/item/item.cpp | 6 ++- src/item/item.hpp | 1 - src/keybindings/actionmanager.cpp | 53 +++++++++--------- src/properties/property.cpp | 4 +- src/properties/property.hpp | 2 +- src/properties/widgets/actionswidget.cpp | 54 +++++++++++-------- src/properties/widgets/actionswidget.hpp | 32 +++++------ src/tools/polygondrawingtool.cpp | 3 +- src/tools/selectiontool/selectiontool.cpp | 7 +-- .../selectiontoolselectstate.cpp | 18 ++++--- 22 files changed, 259 insertions(+), 245 deletions(-) diff --git a/src/command/deselectcommand.cpp b/src/command/deselectcommand.cpp index acf4bcb..a7fa080 100644 --- a/src/command/deselectcommand.cpp +++ b/src/command/deselectcommand.cpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "deselectcommand.hpp" @@ -22,10 +22,11 @@ #include "../context/coordinatetransformer.hpp" #include "../context/selectioncontext.hpp" #include "../context/spatialcontext.hpp" -#include "../item/item.hpp" #include "../data-structures/cachegrid.hpp" +#include "../item/item.hpp" -DeselectCommand::DeselectCommand(QVector> items) : ItemCommand{items} {} +DeselectCommand::DeselectCommand(QVector> items) : ItemCommand{items} { +} DeselectCommand::~DeselectCommand() { } diff --git a/src/command/deselectcommand.hpp b/src/command/deselectcommand.hpp index 661c116..83945de 100644 --- a/src/command/deselectcommand.hpp +++ b/src/command/deselectcommand.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef DESELECTCOMMAND_H #define DESELECTCOMMAND_H diff --git a/src/command/groupcommand.cpp b/src/command/groupcommand.cpp index 3882dc6..ee941b7 100644 --- a/src/command/groupcommand.cpp +++ b/src/command/groupcommand.cpp @@ -1,26 +1,23 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "groupcommand.hpp" -#include "commandhistory.hpp" -#include "selectcommand.hpp" -#include "deselectcommand.hpp" #include "../context/applicationcontext.hpp" #include "../context/coordinatetransformer.hpp" #include "../context/selectioncontext.hpp" @@ -28,6 +25,9 @@ #include "../data-structures/cachegrid.hpp" #include "../data-structures/quadtree.hpp" #include "../item/group.hpp" +#include "commandhistory.hpp" +#include "deselectcommand.hpp" +#include "selectcommand.hpp" GroupCommand::GroupCommand(QVector> items) : ItemCommand{items} { m_group = std::make_shared(); diff --git a/src/command/groupcommand.hpp b/src/command/groupcommand.hpp index 69ccb2c..84a34a3 100644 --- a/src/command/groupcommand.hpp +++ b/src/command/groupcommand.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef GROUPCOMMAND_H #define GROUPCOMMAND_H diff --git a/src/command/selectcommand.cpp b/src/command/selectcommand.cpp index 03bac74..b2a811c 100644 --- a/src/command/selectcommand.cpp +++ b/src/command/selectcommand.cpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "selectcommand.hpp" @@ -22,10 +22,11 @@ #include "../context/coordinatetransformer.hpp" #include "../context/selectioncontext.hpp" #include "../context/spatialcontext.hpp" -#include "../item/item.hpp" #include "../data-structures/cachegrid.hpp" +#include "../item/item.hpp" -SelectCommand::SelectCommand(QVector> items) : ItemCommand{items} {} +SelectCommand::SelectCommand(QVector> items) : ItemCommand{items} { +} SelectCommand::~SelectCommand() { } diff --git a/src/command/selectcommand.hpp b/src/command/selectcommand.hpp index 9669198..40d3f1c 100644 --- a/src/command/selectcommand.hpp +++ b/src/command/selectcommand.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef SELECTCOMMAND_H #define SELECTCOMMAND_H diff --git a/src/command/ungroupcommand.cpp b/src/command/ungroupcommand.cpp index 890cbc9..12052d5 100644 --- a/src/command/ungroupcommand.cpp +++ b/src/command/ungroupcommand.cpp @@ -1,23 +1,25 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "ungroupcommand.hpp" +#include + #include "../context/applicationcontext.hpp" #include "../context/coordinatetransformer.hpp" #include "../context/selectioncontext.hpp" @@ -25,7 +27,6 @@ #include "../data-structures/cachegrid.hpp" #include "../data-structures/quadtree.hpp" #include "../item/group.hpp" -#include UngroupCommand::UngroupCommand(QVector> items) : ItemCommand{items} { for (const auto item : items) { diff --git a/src/command/ungroupcommand.hpp b/src/command/ungroupcommand.hpp index 4dc2b5b..65a4154 100644 --- a/src/command/ungroupcommand.hpp +++ b/src/command/ungroupcommand.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef UNGROUPCOMMAND_H #define UNGROUPCOMMAND_H diff --git a/src/data-structures/quadtree.cpp b/src/data-structures/quadtree.cpp index d2baa1d..f9a1466 100644 --- a/src/data-structures/quadtree.cpp +++ b/src/data-structures/quadtree.cpp @@ -73,7 +73,7 @@ bool QuadTree::insert(std::shared_ptr item, bool updateOrder) { if (m_items.size() < m_capacity) { m_items.push_back(item); - + if (updateOrder) m_orderedList->insert(item); @@ -132,7 +132,7 @@ void QuadTree::clear() { } } -void QuadTree::reorder(QVector& items) const { +void QuadTree::reorder(QVector &items) const { std::sort(items.begin(), items.end(), [&](auto &firstItem, auto &secondItem) { return m_orderedList->zIndex(firstItem) < m_orderedList->zIndex(secondItem); }); diff --git a/src/data-structures/quadtree.hpp b/src/data-structures/quadtree.hpp index 6a41ab3..c860449 100644 --- a/src/data-structures/quadtree.hpp +++ b/src/data-structures/quadtree.hpp @@ -64,7 +64,7 @@ class QuadTree { void updateItem(ItemPtr item, const QRectF &oldBoundingBox); void deleteItems(const QRectF &boundingBox); - void reorder(QVector& items) const; + void reorder(QVector &items) const; QVector getAllItems() const; void clear(); diff --git a/src/item/group.cpp b/src/item/group.cpp index 3e3fd1e..32d6a91 100644 --- a/src/item/group.cpp +++ b/src/item/group.cpp @@ -1,9 +1,11 @@ #include "group.hpp" + #include void GroupItem::draw(QPainter &painter, const QPointF &offset) { for (auto item : m_items) { - item->draw(painter, offset); } + item->draw(painter, offset); + } } void GroupItem::erase(QPainter &painter, const QPointF &offset) const { @@ -18,7 +20,7 @@ void GroupItem::translate(const QPointF &amount) { } } -void GroupItem::group(const QVector>& items) { +void GroupItem::group(const QVector> &items) { m_items = items; } @@ -42,7 +44,6 @@ bool GroupItem::intersects(const QLineF &line) { return false; }; - QVector> GroupItem::unGroup() { return m_items; } @@ -82,7 +83,7 @@ const Property GroupItem::property(const Property::Type propertyType) const { } else { property = item->property(propertyType); } - } catch (const std::logic_error& e) { + } catch (const std::logic_error &e) { // ignore } } diff --git a/src/item/group.hpp b/src/item/group.hpp index e631dd6..e0f8c36 100644 --- a/src/item/group.hpp +++ b/src/item/group.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef GROUP_H #define GROUP_H @@ -33,7 +33,7 @@ class GroupItem : public Item { void translate(const QPointF &amount) override; - void group(const QVector>& items); + void group(const QVector> &items); QVector> unGroup(); void setProperty(const Property::Type propertyType, Property newObj) override; diff --git a/src/item/item.cpp b/src/item/item.cpp index f19a20d..ef22d9f 100644 --- a/src/item/item.cpp +++ b/src/item/item.cpp @@ -73,8 +73,10 @@ void Item::setProperty(const Property::Type propertyType, Property newObj) { updateAfterProperty(); } -void Item::updateAfterProperty() {} -void Item::erase(QPainter &painter, const QPointF &offset) const {} +void Item::updateAfterProperty() { +} +void Item::erase(QPainter &painter, const QPointF &offset) const { +} int Item::boundingBoxPadding() const { return m_boundingBoxPadding; diff --git a/src/item/item.hpp b/src/item/item.hpp index 9e60d5d..3d855d9 100644 --- a/src/item/item.hpp +++ b/src/item/item.hpp @@ -41,7 +41,6 @@ class Item { void setBoundingBoxPadding(int padding); int boundingBoxPadding() const; - virtual void setProperty(const Property::Type propertyType, Property newObj); virtual const Property property(const Property::Type propertyType) const; virtual const QVector properties() const; diff --git a/src/keybindings/actionmanager.cpp b/src/keybindings/actionmanager.cpp index 75b2bc8..7385d0c 100644 --- a/src/keybindings/actionmanager.cpp +++ b/src/keybindings/actionmanager.cpp @@ -17,12 +17,15 @@ */ #include "actionmanager.hpp" -#include "../command/selectcommand.hpp" + +#include + +#include "../command/commandhistory.hpp" #include "../command/deselectcommand.hpp" #include "../command/groupcommand.hpp" -#include "../command/ungroupcommand.hpp" -#include "../command/commandhistory.hpp" #include "../command/removeitemcommand.hpp" +#include "../command/selectcommand.hpp" +#include "../command/ungroupcommand.hpp" #include "../components/propertybar.hpp" #include "../components/toolbar.hpp" #include "../context/applicationcontext.hpp" @@ -37,7 +40,6 @@ #include "../serializer/serializer.hpp" #include "action.hpp" #include "keybindmanager.hpp" -#include ActionManager::ActionManager(ApplicationContext *context) : m_context{context}, QObject(context) { KeybindManager &keybindManager{m_context->uiContext().keybindManager()}; @@ -107,31 +109,27 @@ ActionManager::ActionManager(ApplicationContext *context) : m_context{context}, context}}; Action *groupAction{new Action{"Group Elements", - "Groups selected items", - [&]() { this->groupItems(); }, - context}}; + "Groups selected items", + [&]() { this->groupItems(); }, + context}}; Action *unGroupAction{new Action{"Ungroup Elements", - "Ungroups selected groups", - [&]() { this->ungroupItems(); }, - context}}; + "Ungroups selected groups", + [&]() { this->ungroupItems(); }, + context}}; - Action *selectAllAction{new Action{ - "Select All", - "Select all items", - [&, context]() { this->selectAll(); }, - context}}; + Action *selectAllAction{new Action{"Select All", + "Select all items", + [&, context]() { this->selectAll(); }, + context}}; - Action *deleteAction{new Action{ - "Delete", - "Deletes selected items", - [&, context]() { this->deleteSelection(); }, - context}}; + Action *deleteAction{new Action{"Delete", + "Deletes selected items", + [&, context]() { this->deleteSelection(); }, + context}}; - Action *saveAction{new Action{"Save", - "Save canvas", - [&, context]() { this->saveToFile(); }, - context}}; + Action *saveAction{ + new Action{"Save", "Save canvas", [&, context]() { this->saveToFile(); }, context}}; Action *openFileAction{new Action{"Open File", "Open an existing file", @@ -252,17 +250,14 @@ void ActionManager::deleteSelection() { QVector> selectedItemsVector{selectedItems.begin(), selectedItems.end()}; m_context->spatialContext().commandHistory().insert( - std::make_shared(selectedItemsVector) - ); + std::make_shared(selectedItemsVector)); } void ActionManager::selectAll() { this->switchToSelectionTool(); auto allItems{m_context->spatialContext().quadtree().getAllItems()}; - m_context->spatialContext().commandHistory().insert( - std::make_shared(allItems) - ); + m_context->spatialContext().commandHistory().insert(std::make_shared(allItems)); m_context->uiContext().propertyBar().updateToolProperties(); m_context->renderingContext().markForRender(); diff --git a/src/properties/property.cpp b/src/properties/property.cpp index ef59557..6b0f479 100644 --- a/src/properties/property.cpp +++ b/src/properties/property.cpp @@ -18,8 +18,8 @@ #include "property.hpp" -Property::Property() - : m_type{Property::Null}, m_value{0} {} +Property::Property() : m_type{Property::Null}, m_value{0} { +} Property::Type Property::type() const { return m_type; diff --git a/src/properties/property.hpp b/src/properties/property.hpp index 3f9ef37..a8feca6 100644 --- a/src/properties/property.hpp +++ b/src/properties/property.hpp @@ -24,7 +24,7 @@ class Property { public: Property(); - enum Type { StrokeWidth, StrokeColor, Opacity, FontSize, EraserSize, Actions, Null}; + enum Type { StrokeWidth, StrokeColor, Opacity, FontSize, EraserSize, Actions, Null }; template Property(T value, Type type) { diff --git a/src/properties/widgets/actionswidget.cpp b/src/properties/widgets/actionswidget.cpp index a9ca02a..3b692f9 100644 --- a/src/properties/widgets/actionswidget.cpp +++ b/src/properties/widgets/actionswidget.cpp @@ -1,30 +1,32 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "actionswidget.hpp" + +#include +#include +#include + #include "../../context/applicationcontext.hpp" #include "../../context/uicontext.hpp" -#include "../../keybindings/actionmanager.hpp" #include "../../iconmanager/iconmanager.hpp" +#include "../../keybindings/actionmanager.hpp" #include "../property.hpp" -#include -#include -#include ActionsWidget::ActionsWidget(QWidget *parent) : PropertyWidget{parent} { m_widget = new QWidget{parent}; @@ -55,9 +57,15 @@ ActionsWidget::ActionsWidget(QWidget *parent) : PropertyWidget{parent} { ActionManager &actionManager{ApplicationContext::instance()->uiContext().actionManager()}; - QObject::connect(deleteButton, &QPushButton::clicked, this, [&](){ actionManager.deleteSelection(); }); - QObject::connect(groupButton, &QPushButton::clicked, this, [&](){ actionManager.groupItems(); }); - QObject::connect(ungroupButton, &QPushButton::clicked, this, [&](){ actionManager.ungroupItems(); }); + QObject::connect(deleteButton, &QPushButton::clicked, this, [&]() { + actionManager.deleteSelection(); + }); + QObject::connect(groupButton, &QPushButton::clicked, this, [&]() { + actionManager.groupItems(); + }); + QObject::connect(ungroupButton, &QPushButton::clicked, this, [&]() { + actionManager.ungroupItems(); + }); m_widget->hide(); } diff --git a/src/properties/widgets/actionswidget.hpp b/src/properties/widgets/actionswidget.hpp index 0bbed52..e696bf3 100644 --- a/src/properties/widgets/actionswidget.hpp +++ b/src/properties/widgets/actionswidget.hpp @@ -1,20 +1,20 @@ /* -* Drawy - A simple brainstorming tool with an infinite canvas -* Copyright (C) 2025 - Prayag Jain -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Drawy - A simple brainstorming tool with an infinite canvas + * Copyright (C) 2025 - Prayag Jain + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef ACTIONSWIDGET_H #define ACTIONSWIDGET_H diff --git a/src/tools/polygondrawingtool.cpp b/src/tools/polygondrawingtool.cpp index 2650b91..44731d2 100644 --- a/src/tools/polygondrawingtool.cpp +++ b/src/tools/polygondrawingtool.cpp @@ -19,10 +19,11 @@ #include "polygondrawingtool.hpp" #include -#include "../command/selectcommand.hpp" + #include "../canvas/canvas.hpp" #include "../command/commandhistory.hpp" #include "../command/insertitemcommand.hpp" +#include "../command/selectcommand.hpp" #include "../common/renderitems.hpp" #include "../context/applicationcontext.hpp" #include "../context/coordinatetransformer.hpp" diff --git a/src/tools/selectiontool/selectiontool.cpp b/src/tools/selectiontool/selectiontool.cpp index 7776514..b09a84b 100644 --- a/src/tools/selectiontool/selectiontool.cpp +++ b/src/tools/selectiontool/selectiontool.cpp @@ -16,9 +16,10 @@ * along with this program. If not, see . */ -#include #include "selectiontool.hpp" +#include + #include #include "../../command/commandhistory.hpp" @@ -125,9 +126,9 @@ const QVector SelectionTool::properties() const { QVector output(result.begin(), result.end()); if (!selectedItems.empty()) { - output += QVector{ Property::Actions }; + output += QVector{Property::Actions}; } - + return output; } diff --git a/src/tools/selectiontool/selectiontoolselectstate.cpp b/src/tools/selectiontool/selectiontoolselectstate.cpp index e52f073..269a4eb 100644 --- a/src/tools/selectiontool/selectiontoolselectstate.cpp +++ b/src/tools/selectiontool/selectiontoolselectstate.cpp @@ -17,10 +17,11 @@ */ #include "selectiontoolselectstate.hpp" -#include "../../command/selectcommand.hpp" -#include "../../command/deselectcommand.hpp" -#include "../../command/commandhistory.hpp" + #include "../../canvas/canvas.hpp" +#include "../../command/commandhistory.hpp" +#include "../../command/deselectcommand.hpp" +#include "../../command/selectcommand.hpp" #include "../../components/propertybar.hpp" #include "../../context/applicationcontext.hpp" #include "../../context/coordinatetransformer.hpp" @@ -63,12 +64,15 @@ bool SelectionToolSelectState::mousePressed(ApplicationContext *context) { if (intersectingItems.empty()) { m_isActive = true; } else { - auto& item{intersectingItems.back()}; - if ((event.modifiers() & Qt::ShiftModifier) && selectedItems.find(item) != selectedItems.end()) { + auto &item{intersectingItems.back()}; + if ((event.modifiers() & Qt::ShiftModifier) && + selectedItems.find(item) != selectedItems.end()) { // deselect the item if selected - commandHistory.insert(std::make_shared(QVector>{item})); + commandHistory.insert( + std::make_shared(QVector>{item})); } else { - commandHistory.insert(std::make_shared(QVector>{item})); + commandHistory.insert( + std::make_shared(QVector>{item})); } m_isActive = false; lockState = false; From 507321dea0fbbd16ff1c16acf2bec6161530c86c Mon Sep 17 00:00:00 2001 From: quarterstar Date: Fri, 28 Nov 2025 00:00:00 +0000 Subject: [PATCH 4/8] feat: .clangd file --- .clangd | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..c7100cc --- /dev/null +++ b/.clangd @@ -0,0 +1,4 @@ +CompileFlags: + Add: + - -std=c++23 + - -Wall From 2c0dabb324a2c773f45e33f2b016edadba14c721 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Fri, 28 Nov 2025 00:00:00 +0000 Subject: [PATCH 5/8] docs: make license header more consistent --- LICENSE_HEADER | 2 +- src/common/utils/compression.cpp | 2 +- src/common/utils/compression.hpp | 2 +- src/controller/controller.cpp | 4 ++-- src/controller/controller.hpp | 4 ++-- src/controller/keypress.hpp | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LICENSE_HEADER b/LICENSE_HEADER index 195647b..79cc6c3 100644 --- a/LICENSE_HEADER +++ b/LICENSE_HEADER @@ -3,7 +3,7 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: -* 1. - +* 1. <> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/common/utils/compression.cpp b/src/common/utils/compression.cpp index 6c320bc..c4e90e1 100644 --- a/src/common/utils/compression.cpp +++ b/src/common/utils/compression.cpp @@ -3,7 +3,7 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: - * 1. quarterstar - quarterstar@proton.me + * 1. quarterstar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/common/utils/compression.hpp b/src/common/utils/compression.hpp index 24c1e08..ae57a8f 100644 --- a/src/common/utils/compression.hpp +++ b/src/common/utils/compression.hpp @@ -3,7 +3,7 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: - * 1. quarterstar - quarterstar@proton.me + * 1. quarterstar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index 476a4be..f8c19d4 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -3,8 +3,8 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: - * 1. Prayag Jain - prayagjain2@gmail.com - * 2. quarterstar - quarterstar@proton.me + * 1. Prayag Jain + * 2. quarterstar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/controller/controller.hpp b/src/controller/controller.hpp index 92aba6c..fce4a8c 100644 --- a/src/controller/controller.hpp +++ b/src/controller/controller.hpp @@ -3,8 +3,8 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: - * 1. Prayag Jain - prayagjain2@gmail.com - * 2. quarterstar - quarterstar@proton.me + * 1. Prayag Jain + * 2. quarterstar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/controller/keypress.hpp b/src/controller/keypress.hpp index c99ee79..251d016 100644 --- a/src/controller/keypress.hpp +++ b/src/controller/keypress.hpp @@ -3,7 +3,7 @@ * Copyright (C) 2025 - Prayag Jain * * Authors: - * 1. quarterstar - quarterstar@proton.me + * 1. quarterstar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 4935164815ac67279b94e1f0afaa92eab9901227 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Sat, 29 Nov 2025 00:00:00 +0000 Subject: [PATCH 6/8] fix: allow multiple shortcut combinations for a single action --- src/controller/action.hpp | 43 ++++++++++++++++++++++++++++++++--- src/controller/controller.cpp | 42 ++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/controller/action.hpp b/src/controller/action.hpp index 0e1d60d..d60ca3b 100644 --- a/src/controller/action.hpp +++ b/src/controller/action.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -16,6 +17,42 @@ struct ActionKey { std::variant m_id; bool m_mouse{false}; size_t m_count{1}; + + // NOTE: required by std::set + std::strong_ordering operator<=>(ActionKey const &o) const noexcept { + if (m_id.index() != o.m_id.index()) + return m_id.index() < o.m_id.index() ? std::strong_ordering::less + : std::strong_ordering::greater; + + auto visit_cmp = std::visit( + [](auto const &a, auto const &b) -> std::strong_ordering { + using A = std::decay_t; + using B = std::decay_t; + + if constexpr (!std::is_same_v) { + return std::strong_ordering::equal; + } else { + if (a < b) + return std::strong_ordering::less; + if (b < a) + return std::strong_ordering::greater; + return std::strong_ordering::equal; + } + }, + m_id, + o.m_id); + + if (visit_cmp != std::strong_ordering::equal) + return visit_cmp; + + if (m_mouse != o.m_mouse) + return m_mouse < o.m_mouse ? std::strong_ordering::less : std::strong_ordering::greater; + if (m_count < o.m_count) + return std::strong_ordering::less; + if (m_count > o.m_count) + return std::strong_ordering::greater; + return std::strong_ordering::equal; + } }; inline ActionKey key(Qt::MouseButton key, size_t count = 1) { @@ -29,7 +66,7 @@ inline ActionKey key(Qt::Key key, size_t count = 1) { } struct ActionData { - std::vector m_keys; + std::set> m_keys; bool m_holdRequired{false}; int64_t m_maxGapMs{0}; }; @@ -113,12 +150,12 @@ inline std::vector getDefaultActions(ToolBar &toolbar, Canvas &can result.push_back(ActionFacade{ std::make_unique(toolbar, canvas, Tool::Move), - ActionData{{key(Qt::Key_Space), key(Qt::LeftButton)}, true, -2}, + ActionData{{{key(Qt::Key_Space), key(Qt::LeftButton)}, {key(Qt::MiddleButton)}}, true, -2}, }); result.push_back(ActionFacade{ std::make_unique(toolbar, canvas, Tool::Eraser), - ActionData{{key(Qt::RightButton)}, true, -2}, + ActionData{{{key(Qt::RightButton)}}, true, -2}, }); return result; diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index f8c19d4..c1b0ce5 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -233,38 +233,42 @@ void Controller::keyReleased(QKeyEvent *event) { void Controller::anyPressed(std::variant input) { for (auto &action : m_actions) { - auto keysView = - action.m_data.m_keys | std::views::transform([](auto key) { return key.m_id; }); - std::vector> keys{keysView.begin(), keysView.end()}; - - bool hasAllMoveKeys = action.m_data.m_maxGapMs < 0 - ? this->satisfiesKeys(keys) - : this->satisfiesKeys(keys, std::nullopt); + for (const auto &keyCombination : action.m_data.m_keys) { + auto keysView = + keyCombination | std::views::transform([](auto key) { return key.m_id; }); + std::vector> keys{keysView.begin(), + keysView.end()}; + + bool hasAllMoveKeys = action.m_data.m_maxGapMs < 0 + ? this->satisfiesKeys(keys) + : this->satisfiesKeys(keys, std::nullopt); + + if (m_occupationKeyBuffer.empty() && hasAllMoveKeys) { + m_occupationKeyBuffer = m_keyBuffer; + action.enable(m_taskCtx); + } - if (m_occupationKeyBuffer.empty() && hasAllMoveKeys) { - m_occupationKeyBuffer = m_keyBuffer; - action.enable(m_taskCtx); + action.m_state.m_prevHeld = hasAllMoveKeys; } - - action.m_state.m_prevHeld = hasAllMoveKeys; } } void Controller::anyReleased(std::variant input) { for (auto &action : m_actions) { - bool hasAllMoveKeys = - std::ranges::all_of(action.m_data.m_keys, [&](const auto &requiredKey) { + for (const auto &keyCombination : action.m_data.m_keys) { + bool hasAllMoveKeys = std::ranges::all_of(keyCombination, [&](const auto &requiredKey) { return std::ranges::any_of(m_keyBuffer, [&](const auto &pressedKey) { return pressedKey.m_key == requiredKey.m_id; }); }); - if (!hasAllMoveKeys) { - m_occupationKeyBuffer.clear(); - action.disable(m_taskCtx); - } + if (!hasAllMoveKeys) { + m_occupationKeyBuffer.clear(); + action.disable(m_taskCtx); + } - action.m_state.m_prevHeld = hasAllMoveKeys; + action.m_state.m_prevHeld = hasAllMoveKeys; + } } if (m_keyBuffer.empty()) { From ad625dcae2d0d3666f74581eef9860f1539762c2 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Sat, 29 Nov 2025 00:00:00 +0000 Subject: [PATCH 7/8] build: attempt to fix kanzi windows builds with conditional compilation --- CMakeLists.txt | 23 +++++++++++ deps/CMakeLists.txt | 15 ++++--- src/serializer/loader.cpp | 78 ++++++++++++++++++++++++++--------- src/serializer/serializer.cpp | 14 +++++-- 4 files changed, 100 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e012a63..73a9c44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,25 @@ file(GLOB_RECURSE SRC_FILES "${SRC_DIR}/*.qss" ) +set(COMPRESSION_SUPPORT ON) +if (WIN32) + set(COMPRESSION_SUPPORT OFF) +endif() + +# Exclude compression features for Windows (kanzi not supported) +if(NOT COMPRESSION_SUPPORT) + # list the files to exclude explicitly + set(EXCLUDE_FILES + "${SRC_DIR}/common/utils/compression.cpp" + "${SRC_DIR}/common/utils/compression.hpp" + ) + + # remove them from the main source list + list(REMOVE_ITEM SRC_FILES ${EXCLUDE_FILES}) +endif() + +list(REMOVE_ITEM SRC_FILES ${EXCLUDE_FILES}) + # Combine translation files with the source files. set(PROJECT_SOURCES ${TS_FILES} ${SRC_FILES}) @@ -74,3 +93,7 @@ if (UNIX AND NOT APPLE) install(FILES assets/logo.svg DESTINATION ${CMAKE_INSTALL_FULL_DATADIR}/icons/hicolor/scalable/apps RENAME io.github.prayag2.Drawy.svg) install(FILES deploy/linux/io.github.prayag2.Drawy.metainfo.xml.in DESTINATION ${CMAKE_INSTALL_FULL_DATADIR}/metainfo/ RENAME io.github.prayag2.Drawy.metainfo.xml) endif() + +target_compile_definitions(${PROJECT_NAME} PRIVATE + COMPRESSION_SUPPORT=$ +) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 1fdd4bd..685ea95 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,12 +1,15 @@ include(FetchContent) -FetchContent_Declare( - kanzi - URL https://github.com/flanglet/kanzi-cpp/archive/refs/tags/2.4.0.zip - URL_MD5 ec451cdab40362e9e4af8e5ca780a07d -) +# Kanzi compilation is currently broken on Windows. +if(COMPRESSION_SUPPORT) + FetchContent_Declare( + kanzi + URL https://github.com/flanglet/kanzi-cpp/archive/refs/tags/2.4.0.zip + URL_MD5 ec451cdab40362e9e4af8e5ca780a07d + ) -add_subdirectory(kanzi) + add_subdirectory(kanzi) +endif() # find_package(tinyxml2 REQUIRED) # target_link_libraries(${PROJECT_NAME} PRIVATE tinyxml2::tinyxml2) diff --git a/src/serializer/loader.cpp b/src/serializer/loader.cpp index 5f1a833..f7f6abf 100644 --- a/src/serializer/loader.cpp +++ b/src/serializer/loader.cpp @@ -26,9 +26,9 @@ #include #include #include +#include #include "../common/constants.hpp" -#include "../common/utils/compression.hpp" #include "../context/applicationcontext.hpp" #include "../context/renderingcontext.hpp" #include "../context/spatialcontext.hpp" @@ -42,24 +42,12 @@ #include "../item/rectangle.hpp" #include "../item/text.hpp" -void Loader::loadFromFile(ApplicationContext *context) { - // file filter - QString filter = QString::fromUtf8("Drawy (*.%1)").arg(Common::drawyFileExt); - - // ask for file (handle cancel) - QDir homeDir{QDir::home()}; - QString fileName = QFileDialog::getOpenFileName(nullptr, "Open File", homeDir.path(), filter); - if (fileName.isEmpty()) - return; - - QFile file{fileName}; - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << file.errorString(); - return; - } +#ifdef COMPRESSION_SUPPORT +#include "../common/utils/compression.hpp" +std::optional loadCompressedSave(QFile &file) { QByteArray compressedByteArray = file.readAll(); - file.close(); + QJsonParseError parseError; QByteArray byteArray; try { @@ -73,21 +61,71 @@ void Loader::loadFromFile(ApplicationContext *context) { byteArray = Common::Utils::Compression::decompressData(decoded); } catch (const std::exception &ex2) { qWarning() << "Base64-decode fallback also failed:" << ex2.what(); - return; + return std::nullopt; } } else { - return; + return std::nullopt; } } - QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(byteArray, &parseError); if (doc.isNull() || !doc.isObject()) { qWarning() << "JSON parse failed:" << parseError.errorString() << "offset:" << parseError.offset; + return std::nullopt; + } + + return doc; +} +#endif + +void Loader::loadFromFile(ApplicationContext *context) { + // file filter + QString filter = QString::fromUtf8("Drawy (*.%1)").arg(Common::drawyFileExt); + + // ask for file (handle cancel) + QDir homeDir{QDir::home()}; + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open File", homeDir.path(), filter); + if (fileName.isEmpty()) + return; + + QFile file{fileName}; + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << file.errorString(); + return; + } + + bool jsonLoadFailed = false; + + QByteArray byteArray = file.readAll(); + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(byteArray, &parseError); + if (!doc.isNull() && doc.isObject()) { + jsonLoadFailed = true; + qWarning() << "JSON parse failed:" << parseError.errorString() + << "offset:" << parseError.offset; + } + +#ifdef COMPRESSION_SUPPORT + if (jsonLoadFailed) { + auto maybeDoc = loadCompressedSave(file); + + if (maybeDoc.has_value()) { + doc = maybeDoc.value(); + jsonLoadFailed = false; + } else { + file.close(); + return; + } + } +#endif + + if (jsonLoadFailed) { return; } + file.close(); + QJsonObject docObj = doc.object(); context->reset(); diff --git a/src/serializer/serializer.cpp b/src/serializer/serializer.cpp index e5e3379..3e5a3de 100644 --- a/src/serializer/serializer.cpp +++ b/src/serializer/serializer.cpp @@ -26,7 +26,6 @@ #include #include "../common/constants.hpp" -#include "../common/utils/compression.hpp" #include "../context/applicationcontext.hpp" #include "../context/renderingcontext.hpp" #include "../context/spatialcontext.hpp" @@ -36,6 +35,10 @@ #include "../item/polygon.hpp" #include "../item/text.hpp" +#ifdef COMPRESSION_SUPPORT +#include "../common/utils/compression.hpp" +#endif + Serializer::Serializer() { } @@ -131,14 +134,17 @@ void Serializer::saveToFile() { QFileDialog::getSaveFileName(nullptr, "Save File", defaultFilePath, text.data())}; auto data{doc.toJson(QJsonDocument::Compact)}; - auto compressedData{Common::Utils::Compression::compressData(data)}; + +#ifdef COMPRESSION_SUPPORT + data = Common::Utils::Compression::compressData(data); +#endif QFile file{fileName}; file.open(QIODevice::WriteOnly); - qint64 written = file.write(compressedData); + qint64 written = file.write(data); file.close(); - if (written != compressedData.size()) { + if (written != data.size()) { qWarning() << "Warning: not all bytes were written"; } From f3e5cec02faf244e27feaeeeb89360fbc6467461 Mon Sep 17 00:00:00 2001 From: quarterstar Date: Sat, 29 Nov 2025 00:00:00 +0000 Subject: [PATCH 8/8] Revert "build: attempt to fix kanzi windows builds with conditional compilation" This reverts commit ad625dcae2d0d3666f74581eef9860f1539762c2. --- CMakeLists.txt | 23 ----------- deps/CMakeLists.txt | 15 +++---- src/serializer/loader.cpp | 78 +++++++++-------------------------- src/serializer/serializer.cpp | 14 ++----- 4 files changed, 30 insertions(+), 100 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 73a9c44..e012a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,25 +32,6 @@ file(GLOB_RECURSE SRC_FILES "${SRC_DIR}/*.qss" ) -set(COMPRESSION_SUPPORT ON) -if (WIN32) - set(COMPRESSION_SUPPORT OFF) -endif() - -# Exclude compression features for Windows (kanzi not supported) -if(NOT COMPRESSION_SUPPORT) - # list the files to exclude explicitly - set(EXCLUDE_FILES - "${SRC_DIR}/common/utils/compression.cpp" - "${SRC_DIR}/common/utils/compression.hpp" - ) - - # remove them from the main source list - list(REMOVE_ITEM SRC_FILES ${EXCLUDE_FILES}) -endif() - -list(REMOVE_ITEM SRC_FILES ${EXCLUDE_FILES}) - # Combine translation files with the source files. set(PROJECT_SOURCES ${TS_FILES} ${SRC_FILES}) @@ -93,7 +74,3 @@ if (UNIX AND NOT APPLE) install(FILES assets/logo.svg DESTINATION ${CMAKE_INSTALL_FULL_DATADIR}/icons/hicolor/scalable/apps RENAME io.github.prayag2.Drawy.svg) install(FILES deploy/linux/io.github.prayag2.Drawy.metainfo.xml.in DESTINATION ${CMAKE_INSTALL_FULL_DATADIR}/metainfo/ RENAME io.github.prayag2.Drawy.metainfo.xml) endif() - -target_compile_definitions(${PROJECT_NAME} PRIVATE - COMPRESSION_SUPPORT=$ -) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 685ea95..1fdd4bd 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,15 +1,12 @@ include(FetchContent) -# Kanzi compilation is currently broken on Windows. -if(COMPRESSION_SUPPORT) - FetchContent_Declare( - kanzi - URL https://github.com/flanglet/kanzi-cpp/archive/refs/tags/2.4.0.zip - URL_MD5 ec451cdab40362e9e4af8e5ca780a07d - ) +FetchContent_Declare( + kanzi + URL https://github.com/flanglet/kanzi-cpp/archive/refs/tags/2.4.0.zip + URL_MD5 ec451cdab40362e9e4af8e5ca780a07d +) - add_subdirectory(kanzi) -endif() +add_subdirectory(kanzi) # find_package(tinyxml2 REQUIRED) # target_link_libraries(${PROJECT_NAME} PRIVATE tinyxml2::tinyxml2) diff --git a/src/serializer/loader.cpp b/src/serializer/loader.cpp index f7f6abf..5f1a833 100644 --- a/src/serializer/loader.cpp +++ b/src/serializer/loader.cpp @@ -26,9 +26,9 @@ #include #include #include -#include #include "../common/constants.hpp" +#include "../common/utils/compression.hpp" #include "../context/applicationcontext.hpp" #include "../context/renderingcontext.hpp" #include "../context/spatialcontext.hpp" @@ -42,12 +42,24 @@ #include "../item/rectangle.hpp" #include "../item/text.hpp" -#ifdef COMPRESSION_SUPPORT -#include "../common/utils/compression.hpp" +void Loader::loadFromFile(ApplicationContext *context) { + // file filter + QString filter = QString::fromUtf8("Drawy (*.%1)").arg(Common::drawyFileExt); + + // ask for file (handle cancel) + QDir homeDir{QDir::home()}; + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open File", homeDir.path(), filter); + if (fileName.isEmpty()) + return; + + QFile file{fileName}; + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << file.errorString(); + return; + } -std::optional loadCompressedSave(QFile &file) { QByteArray compressedByteArray = file.readAll(); - QJsonParseError parseError; + file.close(); QByteArray byteArray; try { @@ -61,71 +73,21 @@ std::optional loadCompressedSave(QFile &file) { byteArray = Common::Utils::Compression::decompressData(decoded); } catch (const std::exception &ex2) { qWarning() << "Base64-decode fallback also failed:" << ex2.what(); - return std::nullopt; + return; } } else { - return std::nullopt; + return; } } - QJsonDocument doc = QJsonDocument::fromJson(byteArray, &parseError); - if (doc.isNull() || !doc.isObject()) { - qWarning() << "JSON parse failed:" << parseError.errorString() - << "offset:" << parseError.offset; - return std::nullopt; - } - - return doc; -} -#endif - -void Loader::loadFromFile(ApplicationContext *context) { - // file filter - QString filter = QString::fromUtf8("Drawy (*.%1)").arg(Common::drawyFileExt); - - // ask for file (handle cancel) - QDir homeDir{QDir::home()}; - QString fileName = QFileDialog::getOpenFileName(nullptr, "Open File", homeDir.path(), filter); - if (fileName.isEmpty()) - return; - - QFile file{fileName}; - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << file.errorString(); - return; - } - - bool jsonLoadFailed = false; - - QByteArray byteArray = file.readAll(); QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(byteArray, &parseError); - if (!doc.isNull() && doc.isObject()) { - jsonLoadFailed = true; + if (doc.isNull() || !doc.isObject()) { qWarning() << "JSON parse failed:" << parseError.errorString() << "offset:" << parseError.offset; - } - -#ifdef COMPRESSION_SUPPORT - if (jsonLoadFailed) { - auto maybeDoc = loadCompressedSave(file); - - if (maybeDoc.has_value()) { - doc = maybeDoc.value(); - jsonLoadFailed = false; - } else { - file.close(); - return; - } - } -#endif - - if (jsonLoadFailed) { return; } - file.close(); - QJsonObject docObj = doc.object(); context->reset(); diff --git a/src/serializer/serializer.cpp b/src/serializer/serializer.cpp index 3e5a3de..e5e3379 100644 --- a/src/serializer/serializer.cpp +++ b/src/serializer/serializer.cpp @@ -26,6 +26,7 @@ #include #include "../common/constants.hpp" +#include "../common/utils/compression.hpp" #include "../context/applicationcontext.hpp" #include "../context/renderingcontext.hpp" #include "../context/spatialcontext.hpp" @@ -35,10 +36,6 @@ #include "../item/polygon.hpp" #include "../item/text.hpp" -#ifdef COMPRESSION_SUPPORT -#include "../common/utils/compression.hpp" -#endif - Serializer::Serializer() { } @@ -134,17 +131,14 @@ void Serializer::saveToFile() { QFileDialog::getSaveFileName(nullptr, "Save File", defaultFilePath, text.data())}; auto data{doc.toJson(QJsonDocument::Compact)}; - -#ifdef COMPRESSION_SUPPORT - data = Common::Utils::Compression::compressData(data); -#endif + auto compressedData{Common::Utils::Compression::compressData(data)}; QFile file{fileName}; file.open(QIODevice::WriteOnly); - qint64 written = file.write(data); + qint64 written = file.write(compressedData); file.close(); - if (written != data.size()) { + if (written != compressedData.size()) { qWarning() << "Warning: not all bytes were written"; }