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 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/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 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/common/utils/compression.cpp b/src/common/utils/compression.cpp index 0fc0e5b..c4e90e1 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 + * + * 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..ae57a8f 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 + * + * 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..d60ca3b --- /dev/null +++ b/src/controller/action.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include +#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}; + + // 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) { + 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::set> 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)}, {key(Qt::MiddleButton)}}, 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..c1b0ce5 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 + * 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 * 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,54 @@ 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) { + 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); + } + + action.m_state.m_prevHeld = hasAllMoveKeys; + } + } +} + +void Controller::anyReleased(std::variant input) { + for (auto &action : m_actions) { + 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); + } + + action.m_state.m_prevHeld = hasAllMoveKeys; + } + } + + if (m_keyBuffer.empty()) { + m_occupationKeyBuffer.clear(); + } } void Controller::inputMethodInvoked(QInputMethodEvent *event) { @@ -201,3 +301,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..fce4a8c 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 + * 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 * 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..251d016 --- /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 + * + * 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/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/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/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/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/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; 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()};