From bb512582867027fd5bb1aba64af264946e34b80f Mon Sep 17 00:00:00 2001 From: rolandlo Date: Wed, 12 Oct 2022 12:12:30 +0200 Subject: [PATCH] Add compass --- src/core/control/CompassController.cpp | 80 ++++++++ src/core/control/CompassController.h | 97 +++++++++ src/core/control/Control.cpp | 13 ++ src/core/enums/ActionType.enum.h | 1 + .../enums/generated/ActionType.generated.cpp | 8 + src/core/gui/XournalView.cpp | 19 +- .../gui/inputdevices/CompassInputHandler.cpp | 80 ++++++++ .../gui/inputdevices/CompassInputHandler.h | 42 ++++ .../gui/toolbarMenubar/ToolMenuHandler.cpp | 1 + src/core/model/Compass.cpp | 34 ++++ src/core/model/Compass.h | 62 ++++++ src/core/view/CompassView.cpp | 188 ++++++++++++++++++ src/core/view/CompassView.h | 97 +++++++++ .../hicolor/scalable/actions/xopp-compass.svg | 83 ++++++++ .../hicolor/scalable/actions/xopp-compass.svg | 83 ++++++++ ui/main.glade | 10 + ui/toolbar.ini | 10 +- 17 files changed, 899 insertions(+), 9 deletions(-) create mode 100644 src/core/control/CompassController.cpp create mode 100644 src/core/control/CompassController.h create mode 100644 src/core/gui/inputdevices/CompassInputHandler.cpp create mode 100644 src/core/gui/inputdevices/CompassInputHandler.h create mode 100644 src/core/model/Compass.cpp create mode 100644 src/core/model/Compass.h create mode 100644 src/core/view/CompassView.cpp create mode 100644 src/core/view/CompassView.h create mode 100644 ui/iconsColor-dark/hicolor/scalable/actions/xopp-compass.svg create mode 100644 ui/iconsColor-light/hicolor/scalable/actions/xopp-compass.svg diff --git a/src/core/control/CompassController.cpp b/src/core/control/CompassController.cpp new file mode 100644 index 000000000000..278066d47707 --- /dev/null +++ b/src/core/control/CompassController.cpp @@ -0,0 +1,80 @@ +#include "CompassController.h" + +#include "control/Control.h" +#include "control/layer/LayerController.h" +#include "gui/XournalView.h" +#include "model/Compass.h" +#include "model/GeometryTool.h" +#include "model/Stroke.h" +#include "model/XojPage.h" + +CompassController::CompassController(XojPageView* view, Compass* s): GeometryToolController(view, s) {} + +CompassController::~CompassController() = default; + +auto CompassController::getType() const -> std::string { return "compass"; } + +auto CompassController::posRelToSide(double x, double y) const -> utl::Point { + cairo_matrix_t inv = s->getMatrix(); + cairo_matrix_invert(&inv); + cairo_matrix_transform_point(&inv, &x, &y); + return utl::Point(x, -y); +} + +auto CompassController::isInsideGeometryTool(double x, double y, double border) const -> bool { + const auto p = posRelToSide(x, y); + return std::hypot(p.x, p.y) <= s->getHeight() + border; +} + +auto CompassController::getPointForPos(double a) const -> utl::Point { + cairo_matrix_t matrix = s->getMatrix(); + double x = s->getHeight() * std::cos(a); + double y = s->getHeight() * std::sin(a); + cairo_matrix_transform_point(&matrix, &x, &y); + + return utl::Point(x, y); +} + +void CompassController::createStroke(double a) { + if (!std::isnan(a)) { + angleMax = a; + angleMin = a; + + const auto p = this->getPointForPos(a); + initializeStroke(); + stroke->addPoint(Point(p.x, p.y)); + stroke->addPoint(Point(p.x, p.y)); // doubled point + s->notify(); + } else { + g_warning("No valid stroke from compass!"); + } +} + +void CompassController::updateStroke(double x) { + angleMax = std::max(this->angleMax, x); + angleMin = std::min(this->angleMin, x); + stroke->deletePointsFrom(0); + const auto h = view->getXournal()->getControl()->getToolHandler(); + const auto filled = (h->getFill() != -1); + const auto c = utl::Point{s->getTranslationX(), s->getTranslationY()}; + + if (filled && angleMax < angleMin + 2 * M_PI) { + stroke->addPoint(Point(c.x, c.y)); + } + for (auto i = 0; i <= 100; i++) { + auto p = getPointForPos(angleMin + static_cast(i) / 100.0 * (angleMax - angleMin)); + stroke->addPoint(Point(p.x, p.y)); + } + if (filled && angleMax < angleMin + 2 * M_PI) { + stroke->addPoint(Point(c.x, c.y)); + } + s->notify(); +} + +void CompassController::finalizeStroke() { + angleMax = NAN; + angleMin = NAN; + addStrokeToLayer(); +} + +auto CompassController::existsStroke() -> bool { return !std::isnan(angleMax) && !std::isnan(angleMin); } diff --git a/src/core/control/CompassController.h b/src/core/control/CompassController.h new file mode 100644 index 000000000000..c84cd8364fc9 --- /dev/null +++ b/src/core/control/CompassController.h @@ -0,0 +1,97 @@ +/* + * Xournal++ + * + * A Compass controller + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include + +#include "gui/PageView.h" +#include "util/Point.h" + +#include "GeometryToolController.h" + +class Compass; +class Stroke; + +/** + * @brief A class that controls a compass + + * The compass can be moved, rotated and scaled + * There are methods for translating coordinates + * and methods to deal with the temporary stroke + * that is displayed near the the outline of the + * compass or a radius. + */ + +class CompassController: public GeometryToolController { +public: + CompassController(XojPageView* view, Compass* s); + ~CompassController(); + +public: + std::string getType() const override; + + /** + * @brief returns the position of a point relative to a coordinate system, in which the + * positive x-axis coincides with the distinguished compass axis and the origin lies + * in the center of the compass + * @param x the x-coordinate of the point (in document coordinates) + * @param y the y-coordinate of the point (in document coordinates) + */ + utl::Point posRelToSide(double x, double y) const; + + /** + * @brief checks whether a point with given coordinates lies in the geometry tool with an additional + * border enlarging (or shrinked) it + * @param x the x-coordinate of the given point (in document coordinates) + * @param y the y-coordinate of the given point (in document coordinates) + * @param border the size of the border (if negative, the geometry tool is shrinked via the border) + */ + bool isInsideGeometryTool(double x, double y, double border = 0.0) const override; + + /** + * @brief the point (in document coordinates) for a given angle on the outline of the setsquare + * @param a the angle with respect to the distinguished compass axis of the point + */ + utl::Point getPointForPos(double a) const; + + /** + * @brief creates a stroke starting at the given angle of the outline of the compass + * @param a the angle of the point on the outline of the compass (when unrotated and untranslated) + */ + void createStroke(double a); + + /** + * @brief updates the stroke aligned to the outline of the compass + * @param a the angle of the point on the outline of the compass (when unrotated and untranslated) + * updating the stroke + */ + void updateStroke(double a); + + /** + * @brief finishes the stroke aligned to the outline of the compass + */ + void finalizeStroke(); + + /** + * checks whether a stroke already exists + */ + bool existsStroke(); + +private: + /** + * @brief when a stroke near the outline of the compass is drawn, the minimal and maximal + * angles of the point to be drawn (with respect to an unrotated, and untranslated coordinate system) + * are saved in the variables angleMin and angleMax + */ + double angleMax = NAN; + double angleMin = NAN; +}; diff --git a/src/core/control/Control.cpp b/src/core/control/Control.cpp index 9e483640af54..454062242e4e 100644 --- a/src/core/control/Control.cpp +++ b/src/core/control/Control.cpp @@ -55,6 +55,7 @@ #include "gui/toolbarMenubar/ToolMenuHandler.h" // for Tool... #include "gui/toolbarMenubar/model/ToolbarData.h" // for Tool... #include "gui/toolbarMenubar/model/ToolbarModel.h" // for Tool... +#include "model/Compass.h" // for Sets... #include "model/Document.h" // for Docu... #include "model/DocumentChangeType.h" // for DOCU... #include "model/Element.h" // for Element @@ -85,6 +86,7 @@ #include "util/i18n.h" // for _, FS #include "util/serializing/InputStreamException.h" // for Inpu... #include "util/serializing/ObjectInputStream.h" // for Obje... +#include "view/CompassView.h" // for Sets... #include "view/SetsquareView.h" // for Sets... #include "view/overlays/OverlayView.h" @@ -643,6 +645,17 @@ void Control::actionPerformed(ActionType type, ActionGroup group, GdkEvent* even xournal->getViewFor(getCurrentPageNo())->rerenderPage(); } break; + case ACTION_COMPASS: + if (auto xournal = this->win->getXournal(); + !xournal->getGeometryToolController() || xournal->getGeometryToolController()->getType() != "compass") { + xournal->resetGeometryTool(); + xournal->makeGeometryTool("compass"); + xournal->getViewFor(getCurrentPageNo())->rerenderPage(); + } else { + xournal->resetGeometryTool(); + xournal->getViewFor(getCurrentPageNo())->rerenderPage(); + } + break; case ACTION_TOOL_FLOATING_TOOLBOX: if (enabled) { selectTool(TOOL_FLOATING_TOOLBOX); diff --git a/src/core/enums/ActionType.enum.h b/src/core/enums/ActionType.enum.h index f2c2c8a6e1cb..0e59dbdb8a69 100644 --- a/src/core/enums/ActionType.enum.h +++ b/src/core/enums/ActionType.enum.h @@ -155,6 +155,7 @@ enum ActionType { ACTION_GRID_SNAPPING, ACTION_HIGHLIGHT_POSITION, ACTION_SETSQUARE, + ACTION_COMPASS, // Used for all colors ACTION_SELECT_COLOR, diff --git a/src/core/enums/generated/ActionType.generated.cpp b/src/core/enums/generated/ActionType.generated.cpp index 9137a81a62fe..6674f7ebf75d 100644 --- a/src/core/enums/generated/ActionType.generated.cpp +++ b/src/core/enums/generated/ActionType.generated.cpp @@ -432,6 +432,10 @@ auto ActionType_fromString(const string& value) -> ActionType { return ACTION_SETSQUARE; } + if (value == "ACTION_COMPASS") { + return ACTION_COMPASS; + } + if (value == "ACTION_SELECT_COLOR") { return ACTION_SELECT_COLOR; } @@ -1058,6 +1062,10 @@ auto ActionType_toString(ActionType value) -> string { return "ACTION_SETSQUARE"; } + if (value == ACTION_COMPASS) { + return "ACTION_COMPASS"; + } + if (value == ACTION_SELECT_COLOR) { return "ACTION_SELECT_COLOR"; } diff --git a/src/core/gui/XournalView.cpp b/src/core/gui/XournalView.cpp index c604b0d29cf8..d2b7ab98d766 100644 --- a/src/core/gui/XournalView.cpp +++ b/src/core/gui/XournalView.cpp @@ -10,10 +10,11 @@ #include // for GDK_KEY_Page_Down #include // for g_object_ref_sink -#include "control/Control.h" // for Control -#include "control/PdfCache.h" // for PdfCache -#include "control/ScrollHandler.h" // for ScrollHandler -#include "control/SetsquareController.h" +#include "control/CompassController.h" // for CompassController +#include "control/Control.h" // for Control +#include "control/PdfCache.h" // for PdfCache +#include "control/ScrollHandler.h" // for ScrollHandler +#include "control/SetsquareController.h" // for SetsquareController #include "control/ToolHandler.h" // for ToolHandler #include "control/jobs/XournalScheduler.h" // for XournalScheduler #include "control/settings/MetadataManager.h" // for MetadataManager @@ -25,6 +26,7 @@ #include "enums/ActionType.enum.h" // for ACTION_NONE #include "gui/MainWindow.h" // for MainWindow #include "gui/PdfFloatingToolbox.h" // for PdfFloatingToolbox +#include "gui/inputdevices/CompassInputHandler.h" // for CompassInputHandler #include "gui/inputdevices/HandRecognition.h" // for HandRecognition #include "gui/inputdevices/InputContext.h" // for InputContext #include "gui/inputdevices/SetsquareInputHandler.h" // for SetsquareInputHandler @@ -39,6 +41,7 @@ #include "util/Point.h" // for Point #include "util/Rectangle.h" // for Rectangle #include "util/Util.h" // for npos +#include "view/CompassView.h" // for CompassView #include "view/SetsquareView.h" // for SetsquareView #include "Layout.h" // for Layout @@ -698,6 +701,14 @@ void XournalView::makeGeometryTool(std::string tool) { this->geometryToolHandler = new SetsquareInputHandler(this, this->geometryToolController); geometryToolHandler->registerToPool(setsquare->getHandlerPool()); control->fireActionSelected(GROUP_GEOMETRY_TOOL, ACTION_SETSQUARE); + } else if (tool == "compass") { + auto compass = new Compass(); + view->addOverlayView(std::make_unique(compass, view)); + this->geometryTool = compass; + this->geometryToolController = new CompassController(view, compass); + this->geometryToolHandler = new CompassInputHandler(this, geometryToolController); + geometryToolHandler->registerToPool(compass->getHandlerPool()); + control->fireActionSelected(GROUP_GEOMETRY_TOOL, ACTION_COMPASS); } } diff --git a/src/core/gui/inputdevices/CompassInputHandler.cpp b/src/core/gui/inputdevices/CompassInputHandler.cpp new file mode 100644 index 000000000000..da2fbed50462 --- /dev/null +++ b/src/core/gui/inputdevices/CompassInputHandler.cpp @@ -0,0 +1,80 @@ +#include "CompassInputHandler.h" + +#include // for __shared_ptr_access, shar... + +#include "control/CompassController.h" // for CompassController, HYPOTENUSE +#include "control/Control.h" +#include "control/ToolEnums.h" // for TOOL_HAND, TOOL_HIGHLIGHTER +#include "control/ToolHandler.h" // for ToolHandler +#include "gui/XournalView.h" // for XournalView +#include "gui/inputdevices/InputEvents.h" +#include "model/Compass.h" + +constexpr double MIN_HEIGHT = 1.0; +constexpr double MAX_HEIGHT = 10.0; + +CompassInputHandler::CompassInputHandler(XournalView* xournal, GeometryToolController* controller): + GeometryToolHandler(xournal, controller, Compass::INITIAL_HEIGHT, Compass::INITIAL_X, Compass::INITIAL_Y) {} + +CompassInputHandler::~CompassInputHandler() noexcept { this->unregisterFromPool(); } + +auto CompassInputHandler::handlePointer(InputEvent const& event) -> bool { + const auto coords = getCoords(event); + CompassController* compassController = static_cast(controller); + + const auto toolHandler = xournal->getControl()->getToolHandler(); + switch (toolHandler->getToolType()) { + case TOOL_HIGHLIGHTER: + case TOOL_PEN: + if (event.type == BUTTON_PRESS_EVENT) { + if (controller->isInsideGeometryTool(coords.x, coords.y, 0) && + !controller->isInsideGeometryTool(coords.x, coords.y, -0.5)) { + // initialize range + const auto p = compassController->posRelToSide(coords.x, coords.y); + lastProj = std::atan2(-p.y, p.x); + compassController->createStroke(lastProj); + return true; + } + return false; + } else if (event.type == MOTION_EVENT) { + // update range and paint + if (compassController->existsStroke()) { + const auto p = compassController->posRelToSide(coords.x, coords.y); + auto proj = std::atan2(-p.y, p.x); + proj = lastProj + std::remainder(proj - lastProj, 2 * M_PI); + compassController->updateStroke(proj); + lastProj = proj; + return true; + } + return false; + } else if (event.type == BUTTON_RELEASE_EVENT) { + // add stroke to layer and reset + if (compassController->existsStroke()) { + compassController->finalizeStroke(); + lastProj = NAN; + return true; + } + } + return false; + case TOOL_HAND: + if (event.type == BUTTON_PRESS_EVENT) { + if (!controller->isInsideGeometryTool(coords.x, coords.y, 0)) { + return false; + } else { + sequenceStart(event); + this->handScrolling = true; + return true; + } + } else if (event.type == MOTION_EVENT && this->handScrolling) { + scrollMotion(event); + return true; + } else if (event.type == BUTTON_RELEASE_EVENT && this->handScrolling) { + this->handScrolling = false; + } + default: + return false; + } +} + +auto CompassInputHandler::getMinHeight() const -> double { return MIN_HEIGHT; } +auto CompassInputHandler::getMaxHeight() const -> double { return MAX_HEIGHT; } diff --git a/src/core/gui/inputdevices/CompassInputHandler.h b/src/core/gui/inputdevices/CompassInputHandler.h new file mode 100644 index 000000000000..ae8bd3f87915 --- /dev/null +++ b/src/core/gui/inputdevices/CompassInputHandler.h @@ -0,0 +1,42 @@ +/* + * Xournal++ + * + * Input handler for the compass. + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include "GeometryToolHandler.h" + +class Stroke; +class GeometryToolController; +struct InputEvent; + +/** + * @brief Input handler for the compass + * + * Only the pointer handling (mouse / stylus) is defined in this class. + * The rest comes from the GeometryToolHandler + * */ +class CompassInputHandler: public GeometryToolHandler { + +public: + explicit CompassInputHandler(XournalView* xournalView, GeometryToolController* controller); + ~CompassInputHandler() override; + +private: + /** + * @brief handles input from mouse and stylus for the compass + */ + bool handlePointer(InputEvent const& event) override; + + double lastProj = NAN; + + double getMinHeight() const override; + double getMaxHeight() const override; +}; diff --git a/src/core/gui/toolbarMenubar/ToolMenuHandler.cpp b/src/core/gui/toolbarMenubar/ToolMenuHandler.cpp index 1f1b0dd591fc..19cb69f5d09a 100644 --- a/src/core/gui/toolbarMenubar/ToolMenuHandler.cpp +++ b/src/core/gui/toolbarMenubar/ToolMenuHandler.cpp @@ -409,6 +409,7 @@ void ToolMenuHandler::initToolItems() { addCustomItemTgl("GRID_SNAPPING", ACTION_GRID_SNAPPING, GROUP_GRID_SNAPPING, false, "snapping-grid", _("Grid Snapping")); addCustomItemTgl("SETSQUARE", ACTION_SETSQUARE, GROUP_GEOMETRY_TOOL, false, "setsquare", _("Setsquare")); + addCustomItemTgl("COMPASS", ACTION_COMPASS, GROUP_GEOMETRY_TOOL, false, "compass", _("Compass")); /* * Menu View diff --git a/src/core/model/Compass.cpp b/src/core/model/Compass.cpp new file mode 100644 index 000000000000..328670cd26b3 --- /dev/null +++ b/src/core/model/Compass.cpp @@ -0,0 +1,34 @@ +#include "Compass.h" + +#include // for sin, cos +#include // for initializer_list + +#include "gui/inputdevices/CompassInputHandler.h" +#include "view/CompassView.h" + +Compass::Compass(): Compass(INITIAL_HEIGHT, .0, INITIAL_X, INITIAL_Y) {} + +Compass::Compass(double height, double rotation, double x, double y): GeometryTool(height, rotation, x, y) {} + +Compass::~Compass() { viewPool->dispatchAndClear(xoj::view::CompassView::FINALIZATION_REQUEST, Range()); } + +void Compass::notify() const { + double h = height * CM; + Range rg; + rg.addPoint(translationX - h, translationY - h); + rg.addPoint(translationX + h, translationY + h); + if (this->stroke) { + rg.addPoint(this->stroke->getX(), this->stroke->getY()); + rg.addPoint(this->stroke->getX() + this->stroke->getElementWidth(), + this->stroke->getY() + this->stroke->getElementHeight()); + rg.addPadding(0.5 * this->stroke->getWidth()); + } + Range repaintRange = rg.unite(this->lastRepaintRange); + this->lastRepaintRange = rg; + + viewPool->dispatch(xoj::view::CompassView::UPDATE_VALUES, this->getHeight(), this->getRotation(), + this->getMatrix()); + viewPool->dispatch(xoj::view::CompassView::FLAG_DIRTY_REGION, repaintRange); + handlerPool->dispatch(CompassInputHandler::UPDATE_VALUES, this->getHeight(), this->getRotation(), + this->getTranslationX(), this->getTranslationY()); +} \ No newline at end of file diff --git a/src/core/model/Compass.h b/src/core/model/Compass.h new file mode 100644 index 000000000000..123aca425d56 --- /dev/null +++ b/src/core/model/Compass.h @@ -0,0 +1,62 @@ +/* + * Xournal++ + * + * A compass model + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include "model/GeometryTool.h" + +/** + * @brief A class that models a compass + * + * The compass has the shape of a circle and has a certain height (radius), may be rotated and + * translated. User coordinates are specified in cm. + */ + +namespace xoj::util { +template +class DispatchPool; +} + +class CompassInputHandler; + +namespace xoj::view { +class CompassView; +}; + + +class Compass: public GeometryTool { +public: + Compass(); + + /** + * @brief A compass specified by its radius, rotation angle and translations in x- and y-directions + * @param height the radius of the compass + * @param rotation the angle (in radian) around which the compass is rotated with respect to the x-axis + * @param x the x-coordinate (in pt) of the center of the compass + * @param y the y-coordinate (in pt) of the center of the compass + */ + Compass(double height, double rotation, double x, double y); + + virtual ~Compass(); + + void notify() const override; // calls the update method of all observers + + // parameters used when initially displaying the compass on a page + static constexpr double INITIAL_HEIGHT = 5.0; + static constexpr double INITIAL_X = 21. * HALF_CM; + static constexpr double INITIAL_Y = 15. * HALF_CM; + +private: + /** + * @brief Bounding box of the compass and stroke after its last update + */ + mutable Range lastRepaintRange; +}; diff --git a/src/core/view/CompassView.cpp b/src/core/view/CompassView.cpp new file mode 100644 index 000000000000..8a074254d7ff --- /dev/null +++ b/src/core/view/CompassView.cpp @@ -0,0 +1,188 @@ +#include "CompassView.h" + +#include // for max, min +#include // for isnan, sqrt, cos, sin, atan2 +#include +#include +#include // for move + +#include // for g_error, g_warning + +#include "gui/XournalView.h" // for XournalView +#include "model/Compass.h" // for Compass +#include "model/Stroke.h" // for Stroke +#include "util/raii/CairoWrappers.h" // for CairoSaveGuard +#include "view/View.h" // for Context + +#include "StrokeView.h" // for StrokeView + +using namespace xoj::view; + +constexpr double LINE_WIDTH = .02; +constexpr double FONT_SIZE = .2; + +CompassView::CompassView(const Compass* s, Repaintable* parent): GeometryToolView(s, parent) { + this->registerToPool(s->getViewPool()); +} + +CompassView::~CompassView() noexcept { this->unregisterFromPool(); }; + + +void CompassView::on(FlagDirtyRegionRequest, const Range& rg) { this->parent->flagDirtyRegion(rg); } + +void CompassView::on(UpdateValuesRequest, double h, double rot, cairo_matrix_t m) { + height = h; + rotation = rot; + matrix = m; + radius = height / std::sqrt(2.) - 1.15; + horPosVmarks = std::min(2.5, radius * .6); + minVmark = (std::abs(horPosVmarks - 2.05) < 0.15) ? 5 : 3; + maxVmark = static_cast(std::floor(std::sqrt(cathete(radius, horPosVmarks)) * 10.0) - 2); + maxHmark = static_cast(std::round(height * 10.0)); +} + +void CompassView::deleteOn(CompassView::FinalizationRequest, const Range& rg) { + this->parent->drawAndDeleteToolView(this, rg); +} + +void CompassView::drawGeometryTool(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + cairo_transform(cr, &matrix); + cairo_set_line_width(cr, LINE_WIDTH); + cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); + cairo_select_font_face(cr, "Arial", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, FONT_SIZE); + + cairo_set_source_rgb(cr, 1, 0, 0); // red + drawOutline(cr); + + cairo_set_source_rgb(cr, 0, 0.5, 0); // green + drawHorizontalMarks(cr); + + cairo_set_source_rgb(cr, .5, 0, .5); // violet + drawAngularMarks(cr); + + cairo_set_source_rgb(cr, 0, .5, .5); // turquoise + drawRotation(cr); +} + +void CompassView::drawOutline(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + cairo_move_to(cr, 0, 0); + cairo_arc(cr, 0, 0, this->height, 0.0, 2.0 * M_PI); + cairo_stroke_preserve(cr); + cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.1); + cairo_fill(cr); +} + +void CompassView::drawRotation(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + + // write the angle between hypotenuse and horizontal axis within a small circle + std::stringstream ss; + ss << std::fixed << std::setprecision(1) << std::abs(std::remainder(this->rotation * 180.0 / M_PI, 180.0)); + double ypos = -0.8 * this->height + 0.4; + cairo_move_to(cr, 0, ypos); + showTextCenteredAndRotated(cr, ss.str(), -this->rotation * 180.0 / M_PI); + + cairo_move_to(cr, 0, ypos); + cairo_new_sub_path(cr); + cairo_arc(cr, 0, ypos, 0.3, 0.0, 2.0 * M_PI); + cairo_stroke(cr); +} + +void CompassView::drawAngularMarks(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + + for (int i = 0; i < 360; i++) { + const double x = this->height; + const double t = ((i % 5) == 0) / 10.0 + .1; + cairo_move_to(cr, x * cos(i * M_PI / 180), x * sin(i * M_PI / 180)); + if (i % 30 == 0) { + const double rad5 = (i == 270) ? (this->height * 0.9 - 0.2) : (this->height * 0.3 + 0.3); + cairo_line_to(cr, rad5 * cos(i * M_PI / 180), rad5 * sin(i * M_PI / 180)); + if (i != 0) { + cairo_move_to(cr, this->height * 0.3 * cos(i * M_PI / 180), this->height * 0.3 * sin(i * M_PI / 180)); + showTextCenteredAndRotated(cr, std::to_string(360 - i), i + 90); + } + } else { + cairo_rel_line_to(cr, -t * cos(i * M_PI / 180), -t * sin(i * M_PI / 180)); + } + } + cairo_stroke(cr); +} + +void CompassView::drawVerticalMarks(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + // BEGIN: vertical marks in center + const auto max = this->maxVmark / 10; // number of full centimeters + for (auto i = 1; i <= max; i++) { + cairo_move_to(cr, 0, i - .25); + cairo_rel_line_to(cr, 0, .5); + } + cairo_stroke(cr); + // END: vertical marks in center + + // BEGIN: VERTICAL marks within circle + // clip vertical stripes + cairo_rectangle(cr, -this->horPosVmarks - 0.25, 0.25, .75, this->maxVmark / 10 + 0.25); + cairo_rectangle(cr, this->horPosVmarks - 0.5, 0.25, .75, this->maxVmark / 10 + 0.25); + cairo_rectangle(cr, -.25, 0.25, .5, this->maxVmark / 10 + 0.25); + cairo_rectangle(cr, -this->height, 0, 2 * this->height, this->height); // clip to the outside + cairo_clip(cr); + + + // draw marks + for (auto i = .5; i <= max; i += .5) { + const double x = cathete(this->radius - 0.25, i); + cairo_move_to(cr, -x, i); + cairo_line_to(cr, x, i); + cairo_stroke(cr); + } + cairo_reset_clip(cr); + // END: VERTICAL marks within circle + + + // BEGIN: minor vertical marks with numbers + cairo_select_font_face(cr, "Arial", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, .2); + + for (int i = this->minVmark; i <= this->maxVmark; i++) { + // double x = sqrt(pow(90, 2) - pow(2 * i, 2)); + const int n = (int)floor(i / 10); + std::string text = std::to_string(n); + const int j = (i % 5 == 0); + cairo_move_to(cr, -this->horPosVmarks, (double)i / 10.0); + cairo_rel_line_to(cr, .1 + j / 10.0, 0); + if (i % 10 == 0) { + cairo_rel_move_to(cr, .12, .06); + cairo_show_text(cr, text.c_str()); + }; + cairo_move_to(cr, this->horPosVmarks, (double)i / 10.0); + cairo_rel_line_to(cr, (-.1 - j / 10.0), 0); + if (i % 10 == 0) { + cairo_rel_move_to(cr, -.24, .06); + cairo_show_text(cr, text.c_str()); + }; + } + cairo_stroke(cr); + // END: minor vertical marks with numbers +} + +void CompassView::drawHorizontalMarks(cairo_t* cr) const { + xoj::util::CairoSaveGuard saveGuard(cr); + // BEGIN: radial measuring marks + + for (int i = 0; i <= this->maxHmark; i++) { + const int n = abs(i / 10); + const double t = ((i % 5) == 0) / 10.0 + .1; + cairo_move_to(cr, (double)i / 10.0, 0); + cairo_rel_line_to(cr, 0, t); + if (i % 10 == 0) { + cairo_rel_move_to(cr, -0.06, 0.18); + cairo_show_text(cr, std::to_string(n).c_str()); + } + } + cairo_stroke(cr); + // END: radial measuring marks +} diff --git a/src/core/view/CompassView.h b/src/core/view/CompassView.h new file mode 100644 index 000000000000..e2cd764fd7be --- /dev/null +++ b/src/core/view/CompassView.h @@ -0,0 +1,97 @@ +/* + * Xournal++ + * + * A Compass view + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include // for cairo_t + +#include "model/Compass.h" // for Compass +#include "util/DispatchPool.h" +#include "view/overlays/BaseStrokeToolView.h" + +#include "GeometryToolView.h" + +class Stroke; +class XojPageView; +class OverlayBase; + +/** + * @brief A class that renders a compass + * + * The compass has vertical, horizontal and angular marks. + * The intersection angle with the x-axis is displayed as well. + * + * See the file development/documentation/Compass_Readme.md for + * details how the Compass is rendered. + * + * A temporary stroke is displayed when it is created near the + * longest side of the Compass or from a point to the mid point + * of the longest side of the Compass. + */ + +namespace xoj::view { +class Repaintable; + +class CompassView: public GeometryToolView { + +public: + CompassView(const Compass* s, Repaintable* parent); + ~CompassView() override; + + void on(FlagDirtyRegionRequest, const Range& rg) override; + void on(UpdateValuesRequest, double h, double rot, cairo_matrix_t m) override; + void deleteOn(FinalizationRequest, const Range& rg) override; + +private: + void drawGeometryTool(cairo_t* cr) const override; + + double height = Compass::INITIAL_HEIGHT; + double rotation = 0.; + cairo_matrix_t matrix{CM, 0., 0., CM, Compass::INITIAL_X, Compass::INITIAL_Y}; + + /** + * @brief the radius of the semi-circle for the angular marks + */ + double radius = 4.5; + + /** + * @brief the distance (in cm) of the vertical marks from the symmetry axis of the setsquare + */ + double horPosVmarks = 2.5; + + /** + * @brief the index of the first vertical mark which should be drawn (which should not overlap with the measuring + * marks) + */ + int minVmark = 3; + + /** + * @brief the index of the last vertical mark to be drawn + */ + int maxVmark = 35; + + /** + * @brief the index of the last horizontal mark to be drawn + */ + int maxHmark = 10 * Compass::INITIAL_HEIGHT; + + void drawVerticalMarks(cairo_t* cr) const; + void drawHorizontalMarks(cairo_t* cr) const; + void drawAngularMarks(cairo_t* cr) const; + void drawOutline(cairo_t* cr) const; + void drawRotation(cairo_t* cr) const; + + /** + * @brief updates the values radius, horPosVmarks, minVmark, maxVmark computed from the height of the Compass + */ + void updateValues(double h, double rot, cairo_matrix_t m); +}; +}; // namespace xoj::view \ No newline at end of file diff --git a/ui/iconsColor-dark/hicolor/scalable/actions/xopp-compass.svg b/ui/iconsColor-dark/hicolor/scalable/actions/xopp-compass.svg new file mode 100644 index 000000000000..f49109444b90 --- /dev/null +++ b/ui/iconsColor-dark/hicolor/scalable/actions/xopp-compass.svg @@ -0,0 +1,83 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/iconsColor-light/hicolor/scalable/actions/xopp-compass.svg b/ui/iconsColor-light/hicolor/scalable/actions/xopp-compass.svg new file mode 100644 index 000000000000..bae94d49b0da --- /dev/null +++ b/ui/iconsColor-light/hicolor/scalable/actions/xopp-compass.svg @@ -0,0 +1,83 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/main.glade b/ui/main.glade index 3526df7e04e8..94d608896d32 100644 --- a/ui/main.glade +++ b/ui/main.glade @@ -1616,6 +1616,16 @@ + + + False + menuCompass + True + False + Compass + + + True diff --git a/ui/toolbar.ini b/ui/toolbar.ini index 59d5dfad381d..be002a5eba48 100755 --- a/ui/toolbar.ini +++ b/ui/toolbar.ini @@ -40,7 +40,7 @@ name[de] = Hochformat name[it] = Ritratto name[ru] = Портретная toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR, CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, GOTO_FIRST,GOTO_BACK,GOTO_NEXT_ANNOTATED_PAGE,GOTO_NEXT,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR ,FULLSCREEN,SEPARATOR, AUDIO_RECORDING,AUDIO_SEEK_BACKWARDS,AUDIO_PAUSE_PLAYBACK,AUDIO_STOP_PLAYBACK,AUDIO_SEEK_FORWARDS,SEPARATOR, SELECT_FONT -toolbarTop2 = PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, ROTATION_SNAPPING,GRID_SNAPPING,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,SEPARATOR, DEFAULT_TOOL,SEPARATOR, FINE,MEDIUM,THICK,SEPARATOR,TOOL_FILL,SEPARATOR,COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT +toolbarTop2 = PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, ROTATION_SNAPPING,GRID_SNAPPING,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,COMPASS,SEPARATOR, DEFAULT_TOOL,SEPARATOR, FINE,MEDIUM,THICK,SEPARATOR,TOOL_FILL,SEPARATOR,COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT toolbarBottom1 = PAGE_SPIN,SEPARATOR,LAYER, SPACER, PAIRED_PAGES,PRESENTATION_MODE,ZOOM_100,ZOOM_FIT,ZOOM_OUT,ZOOM_SLIDER,ZOOM_IN [Minimal Left] @@ -62,7 +62,7 @@ name = Xournal++ name[de] = Xournal++ name[it] = Xournal++ name[ru] = Xournal++ -toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,SEPARATOR, FINE,MEDIUM,THICK,SEPARATOR,TOOL_FILL,SEPARATOR ,COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT, SEPARATOR, SELECT_FONT +toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,COMPASS,SEPARATOR, FINE,MEDIUM,THICK,SEPARATOR,TOOL_FILL,SEPARATOR ,COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT, SEPARATOR, SELECT_FONT toolbarBottom1 = PAGE_SPIN,SEPARATOR,LAYER,GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SPACER, PAIRED_PAGES,PRESENTATION_MODE,ZOOM_100,ZOOM_FIT,ZOOM_OUT,ZOOM_SLIDER,ZOOM_IN,SEPARATOR, FULLSCREEN [Right hand Note Taking] @@ -79,7 +79,7 @@ name = Toolbar Left name[de] = Toolbar Links name[it] = Barra a Sinistra name[ru] = Панель инструментов слева -toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN +toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,COMPASS,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN toolbarLeft1 = COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT,SEPARATOR, FINE,MEDIUM,THICK, SEPARATOR,PAIRED_PAGES,PRESENTATION_MODE,ZOOM_100,ZOOM_FIT,ZOOM_OUT,ZOOM_SLIDER,ZOOM_IN @@ -88,14 +88,14 @@ name = Toolbar Right name[de] = Toolbar Rechts name[it] = Barra a Destra name[ru] = Панель инструментов справа -toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN +toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR, UNDO,REDO,SEPARATOR, PEN,ERASER,HIGHLIGHTER,PDF_TOOL,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,COMPASS,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN toolbarRight1 = COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT,SEPARATOR, FINE,MEDIUM,THICK, SEPARATOR,PAIRED_PAGES,PRESENTATION_MODE,ZOOM_100,ZOOM_FIT,ZOOM_OUT,ZOOM_SLIDER,ZOOM_IN [Floating Toolbox (experimental)] name=Floating Toolbox (experimental) name[de] = Schwebende Toolbox name[ru] = Плавающая панель инструментов (экспериментальная) -toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR,ERASER,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN +toolbarTop1 = SAVE,NEW,OPEN,SEPARATOR,SAVEPDF,PRINT,SEPARATOR,CUT,COPY,PASTE,SEPARATOR,ERASER,IMAGE,TEXT,MATH_TEX,DRAW,SEPARATOR, SELECT,VERTICAL_SPACE,HAND,SETSQUARE,COMPASS,SEPARATOR, DEFAULT_TOOL,SEPARATOR, PAGE_SPIN,SEPARATOR, GOTO_FIRST,GOTO_NEXT_ANNOTATED_PAGE,GOTO_LAST,INSERT_NEW_PAGE,DELETE_CURRENT_PAGE,SEPARATOR, LAYER,FULLSCREEN toolbarLeft1 = COLOR(0),COLOR(1),COLOR(2),COLOR(3),COLOR(4),COLOR(5),COLOR(6),COLOR(7),COLOR(8),COLOR(9),COLOR(10),COLOR_SELECT,SEPARATOR, SEPARATOR,PAIRED_PAGES,PRESENTATION_MODE,ZOOM_100,ZOOM_FIT,ZOOM_OUT,ZOOM_IN toolbarFloat1=PEN,HIGHLIGHTER,PDF_TOOL,UNDO toolbarFloat2=ZOOM_SLIDER,DRAW_ELLIPSE