From 3e5409f0c87c064b8fc8c5a441fda9f87d8aa21b Mon Sep 17 00:00:00 2001 From: Ft Omara Date: Tue, 4 Mar 2025 13:19:50 +0000 Subject: [PATCH 01/11] Update .gitlab-ci.yml file in inkscape:linux made timeout 3h and the cmake config to -2j instead of -3j --- .gitlab-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 754d0c03a8..ba0c519449 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -88,9 +88,9 @@ inkscape:linux: <<: *ccache_init script: - *cmake - - make -j3 install - - make -j3 tests - - make -j3 unit_tests + - make -j2 install + - make -j2 tests + - make -j2 unit_tests #- cpack -G DEB - rm -rf src _CPack_Packages # exclude from artifacts - cd .. @@ -98,6 +98,7 @@ inkscape:linux: expire_in: 1 year paths: - build/ + timeout: 3h appimage:linux: -- GitLab From 297ea08984dc76b87e9688825da52904ece55959 Mon Sep 17 00:00:00 2001 From: PBS Date: Wed, 5 Mar 2025 02:33:09 +0100 Subject: [PATCH 02/11] Make default border colour transparent Previous colour was solid black, which sometimes lingered on startup. Fixes https://gitlab.com/inkscape/inkscape/-/issues/5557 --- src/ui/widget/canvas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index bec9098c86..57d1cf1f47 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -260,7 +260,7 @@ public: // Various state affecting what is drawn. uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard. - uint32_t border = 0x000000ff; // The border colour, used only to control shadow colour. + uint32_t border = 0x00000000; // The border colour, used only to control shadow colour. uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard. bool clip_to_page = false; // Whether to enable clip-to-page mode. -- GitLab From cce7d4ceb9e5d379f25017e6a423b66c60ad5996 Mon Sep 17 00:00:00 2001 From: KrIr17 Date: Tue, 4 Mar 2025 10:47:06 +0530 Subject: [PATCH 03/11] Set timeouts for build jobs explicitly Setting `timeout` in the default job does not work [0] hence set it explicitly for jobs that take time. [0] https://gitlab.com/gitlab-org/gitlab/-/issues/213634 --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ba0c519449..abd9163df6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -78,6 +78,7 @@ workflow: inkscape:linux: stage: build + timeout: 3h rules: - *do_not_run_for_schedules - *run_otherwise @@ -103,6 +104,7 @@ inkscape:linux: appimage:linux: stage: build + timeout: 3h rules: - *do_not_run_for_schedules - *run_otherwise @@ -175,6 +177,7 @@ inkscape:macos: inkscape:windows: extends: .windows + timeout: 3h retry: 2 parallel: matrix: -- GitLab From 2cde653662133618b585d211a2b2872969a10383 Mon Sep 17 00:00:00 2001 From: Tavmjong Bah Date: Wed, 5 Mar 2025 11:19:11 +0100 Subject: [PATCH 04/11] Prevent empty sub-paths from causing segfaults in LPEPowerStroke. An empty sub-path ("M x, y Z") causes trouble when trying to count the number of curves in the sub-path when the code attempts to check if the path's closing curve (line) is degenerate (degenerate closing paths are not counted). The function Geom::Path::back_closed() can't return a valid curve. To avoid a segfault, using Geom::Path::back_default() might be better but it would return an empty (degenerate) curve. Better to just ignore empty sub-paths. This fixes the code in two place: in the LPEPowerStroke::doEffect_path function as well as more generically in the function count_path_curves() (geom.cpp). For good measure, it also adds checks to count_path_nodes() and count_path_degenerations(). Fixes #5263. --- src/helper/geom.cpp | 17 +++++++++++++++++ src/live_effects/lpe-powerstroke.cpp | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/helper/geom.cpp b/src/helper/geom.cpp index 3b67d4ea61..6756391275 100644 --- a/src/helper/geom.cpp +++ b/src/helper/geom.cpp @@ -765,6 +765,12 @@ count_pathvector_degenerations(Geom::PathVector const &pathv) { size_t count_path_degenerations(Geom::Path const &path) { + if (path.empty()) { + std::cerr << "count_path_degenerates: path is empty!" << std::endl; + // Hmm, a path always contains a closing segment which has two nodes which are degenerate if path is empty. + return 0; + } + size_t tot = 0; Geom::Path::const_iterator curve_it = path.begin(); Geom::Path::const_iterator curve_endit = path.end_default(); @@ -791,6 +797,12 @@ size_t count_path_degenerations(Geom::Path const &path) size_t count_path_nodes(Geom::Path const &path) { + if (path.empty()) { + std::cerr << "count_path_nodes: path is empty!" << std::endl; + // Hmm, a path always contains a closing segment which has two (degenerate) nodes... + return 0; + } + size_t tot = path.size_default() + 1; // if degenerate closing line one is erased no need to duple if (path.closed()) { tot -= 1; @@ -810,6 +822,11 @@ size_t count_path_nodes(Geom::Path const &path) size_t count_path_curves(Geom::Path const &path) { + if (path.empty()) { + std::cerr << "count_path_curves: path is empty!" << std::endl; + return 0; + } + size_t tot = path.size_default(); // if degenerate closing line one is erased no need to duple if (path.closed()) { auto const &closingline = path.back_closed(); diff --git a/src/live_effects/lpe-powerstroke.cpp b/src/live_effects/lpe-powerstroke.cpp index 70d49d48d4..d09145907f 100644 --- a/src/live_effects/lpe-powerstroke.cpp +++ b/src/live_effects/lpe-powerstroke.cpp @@ -628,6 +628,10 @@ LPEPowerStroke::doEffect_path (Geom::PathVector const & path_in) offset_points.set_pwd2(pwd2_in , n); size_t pathindex = 0; for (auto path : pathv) { + if (path.empty()) { + std::cerr << "LPEPowerStroke::doEffect_path: empty sub-path!" << std::endl; + continue; + } size_t psize = count_pathvector_curves(path); path_init += psize; if (!offset_points.unplaced && -- GitLab From 235c62cc83de5ca26cbe710b3b95d7d0f759893e Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Sat, 22 Feb 2025 20:41:23 +0100 Subject: [PATCH 05/11] Async selection: notify about selection updates --- src/extension/implementation/script.cpp | 185 ++++++++++++++---------- src/extension/implementation/script.h | 7 + 2 files changed, 116 insertions(+), 76 deletions(-) diff --git a/src/extension/implementation/script.cpp b/src/extension/implementation/script.cpp index c710aee0e2..eca5764968 100644 --- a/src/extension/implementation/script.cpp +++ b/src/extension/implementation/script.cpp @@ -819,11 +819,10 @@ int Script::execute(std::list const &in_command, std::listset_buffered(false); watch.emplace(std::move(stdin_channel)); - document->addUndoObserver(*watch); - + (*watch).connect(desktop, document); auto on_lose_document = [&] { KILL_PROCESS(local_pid); - document->removeUndoObserver(*watch); + (*watch).disconnect(document); lost_document = true; conns.clear(); }; @@ -836,7 +835,7 @@ int Script::execute(std::list const &in_command, std::listrun(); if (pipe_diffs && !lost_document) { - document->removeUndoObserver(*watch); + (*watch).disconnect(document); } // Ensure all the data is out of the pipe @@ -930,6 +929,50 @@ Script::PreviewObserver::PreviewObserver(Glib::RefPtr channel) : _channel{std::move(channel)} {} +void Script::PreviewObserver::connect(SPDesktop const *desktop, SPDocument *document) +{ + document->addUndoObserver(*this); + auto selection = desktop->getSelection(); + _select_changed = + selection->connectChanged([this](Inkscape::Selection *selection) { selectionChanged(selection); }); +} + +void Script::PreviewObserver::disconnect(SPDocument *document) +{ + document->removeUndoObserver(*this); + _select_changed.disconnect(); +} + +void Script::PreviewObserver::createAndSendEvent( + std::function const &eventPopulator) +{ + Inkscape::XML::Document *doc = new Inkscape::XML::SimpleDocument(); + Inkscape::XML::Node *event_node = doc->createElement("event"); + doc->addChildAtPos(event_node, 0); + Inkscape::GC::release(event_node); + + eventPopulator(doc, event_node); + + Glib::ustring xml_output = sp_repr_write_buf(event_node, 0, true, GQuark(0), 0, 0); + _channel->write(xml_output + "\n"); + + Inkscape::GC::release(doc); + delete doc; +} + +void Script::PreviewObserver::selectionChanged(Inkscape::Selection *selection) +{ + createAndSendEvent([&](Inkscape::XML::Document *doc, Inkscape::XML::Node *event_node) { + event_node->setAttribute("type", "updateSelection"); + for (auto objsel : selection->objects()) { + Inkscape::XML::Node *item = event_node->document()->createElement("selObj"); + item->setAttribute("id", objsel->getId()); + event_node->appendChild(item); + Inkscape::GC::release(item); + } + }); +} + void Script::PreviewObserver::notifyUndoCommitEvent(Event *ee) { std::vector events; @@ -941,80 +984,70 @@ void Script::PreviewObserver::notifyUndoCommitEvent(Event *ee) // Process events in reverse order (chronological order) for (auto e : events | boost::adaptors::reversed) { - Inkscape::XML::Document *doc = new Inkscape::XML::SimpleDocument(); - XML::Node *event_node = doc->createElement("event"); - doc->addChildAtPos(event_node, 0); - Inkscape::GC::release(event_node); - - // Add type and element ID - if (auto eadd = dynamic_cast(e)) { - event_node->setAttribute("type", "add"); - if (eadd->ref) { - event_node->setAttribute("after", eadd->ref->attribute("id")); - } - if (eadd->child) { - Inkscape::XML::Node *new_child = eadd->child->duplicate(doc); + createAndSendEvent([&](Inkscape::XML::Document *doc, Inkscape::XML::Node *event_node) { + if (auto eadd = dynamic_cast(e)) { + event_node->setAttribute("type", "add"); + if (eadd->ref) { + event_node->setAttribute("after", eadd->ref->attribute("id")); + } + if (eadd->child) { + Inkscape::XML::Node *new_child = eadd->child->duplicate(doc); - event_node->appendChild(new_child); - Inkscape::GC::release(new_child); - } - if (eadd->repr) { - event_node->setAttribute("parent", eadd->repr->attribute("id")); - } - } else if (auto edel = dynamic_cast(e)) { - event_node->setAttribute("type", "delete"); - if (edel->repr && edel->repr->attribute("id")) { - event_node->setAttribute("parent", edel->repr->attribute("id")); - } - if (edel->ref) { - event_node->setAttribute("after", edel->ref->attribute("id")); - } - if (edel->child) { - event_node->setAttribute("child", edel->child->attribute("id")); - } - } else if (auto echga = dynamic_cast(e)) { - event_node->setAttribute("type", "attribute_change"); - if (echga->repr && e->repr->attribute("id")) { - event_node->setAttribute("element-id", echga->repr->attribute("id")); - } - event_node->setAttribute("attribute-name", g_quark_to_string(echga->key)); - event_node->setAttribute("old-value", &*(echga->oldval)); - event_node->setAttribute("new-value", &*(echga->newval)); - } else if (auto echgc = dynamic_cast(e)) { - event_node->setAttribute("type", "content_change"); - if (e->repr && e->repr->attribute("id")) { - event_node->setAttribute("element-id", e->repr->attribute("id")); - } - event_node->setAttribute("old-content", &*(echgc->oldval)); - event_node->setAttribute("new-content", &*(echgc->newval)); - } else if (auto echgo = dynamic_cast(e)) { - event_node->setAttribute("type", "order_change"); - if (echgo->repr && echgo->repr->attribute("id")) { - event_node->setAttribute("element-id", e->repr->attribute("id")); - } - event_node->setAttribute("child", echgo->child->attribute("id")); - if (echgo->oldref) { - event_node->setAttribute("old-ref", echgo->oldref->attribute("id")); - } - if (echgo->newref) { - event_node->setAttribute("new-ref", echgo->newref->attribute("id")); - } - } else if (auto echgn = dynamic_cast(e)) { - event_node->setAttribute("type", "element_name_change"); - if (echgn->repr && echgn->repr->attribute("id")) { - event_node->setAttribute("element-id", e->repr->attribute("id")); + event_node->appendChild(new_child); + Inkscape::GC::release(new_child); + } + if (eadd->repr) { + event_node->setAttribute("parent", eadd->repr->attribute("id")); + } + } else if (auto edel = dynamic_cast(e)) { + event_node->setAttribute("type", "delete"); + if (edel->repr && edel->repr->attribute("id")) { + event_node->setAttribute("parent", edel->repr->attribute("id")); + } + if (edel->ref) { + event_node->setAttribute("after", edel->ref->attribute("id")); + } + if (edel->child) { + event_node->setAttribute("child", edel->child->attribute("id")); + } + } else if (auto echga = dynamic_cast(e)) { + event_node->setAttribute("type", "attribute_change"); + if (echga->repr && e->repr->attribute("id")) { + event_node->setAttribute("element-id", echga->repr->attribute("id")); + } + event_node->setAttribute("attribute-name", g_quark_to_string(echga->key)); + event_node->setAttribute("old-value", &*(echga->oldval)); + event_node->setAttribute("new-value", &*(echga->newval)); + } else if (auto echgc = dynamic_cast(e)) { + event_node->setAttribute("type", "content_change"); + if (e->repr && e->repr->attribute("id")) { + event_node->setAttribute("element-id", e->repr->attribute("id")); + } + event_node->setAttribute("old-content", &*(echgc->oldval)); + event_node->setAttribute("new-content", &*(echgc->newval)); + } else if (auto echgo = dynamic_cast(e)) { + event_node->setAttribute("type", "order_change"); + if (echgo->repr && echgo->repr->attribute("id")) { + event_node->setAttribute("element-id", e->repr->attribute("id")); + } + event_node->setAttribute("child", echgo->child->attribute("id")); + if (echgo->oldref) { + event_node->setAttribute("old-ref", echgo->oldref->attribute("id")); + } + if (echgo->newref) { + event_node->setAttribute("new-ref", echgo->newref->attribute("id")); + } + } else if (auto echgn = dynamic_cast(e)) { + event_node->setAttribute("type", "element_name_change"); + if (echgn->repr && echgn->repr->attribute("id")) { + event_node->setAttribute("element-id", e->repr->attribute("id")); + } + event_node->setAttribute("old-name", g_quark_to_string(echgn->old_name)); + event_node->setAttribute("new-name", g_quark_to_string(echgn->new_name)); + } else { + event_node->setAttribute("type", "unknown"); } - event_node->setAttribute("old-name", g_quark_to_string(echgn->old_name)); - event_node->setAttribute("new-name", g_quark_to_string(echgn->new_name)); - } else { - event_node->setAttribute("type", "unknown"); - } - - Glib::ustring xml_output = sp_repr_write_buf(event_node, 0, true, GQuark(0), 0, 0); - _channel->write(xml_output + "\n"); - - Inkscape::GC::release(doc); - delete doc; + }); } } diff --git a/src/extension/implementation/script.h b/src/extension/implementation/script.h index 6a28cc6ef5..b1b8dbc0e6 100644 --- a/src/extension/implementation/script.h +++ b/src/extension/implementation/script.h @@ -26,6 +26,7 @@ #include #include "implementation.h" +#include "selection.h" #include "undo-stack-observer.h" #include "xml/node.h" @@ -122,15 +123,21 @@ private: { public: PreviewObserver(Glib::RefPtr channel); + void connect(SPDesktop const *desktop, SPDocument *document); + void disconnect(SPDocument *document); private: + void selectionChanged(Inkscape::Selection *selection); void notifyUndoCommitEvent(Event *log) override; void notifyUndoEvent(Event *log) override; void notifyRedoEvent(Event *log) override; void notifyClearUndoEvent() override; void notifyClearRedoEvent() override; void notifyUndoExpired(Event *log) override; + void createAndSendEvent( + std::function const &eventPopulator); + sigc::connection _select_changed; Glib::RefPtr _channel; }; -- GitLab From bd24bb2e7bf6a715cc916d38aed5633464b1edf1 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Tue, 25 Feb 2025 14:32:49 +0100 Subject: [PATCH 06/11] Refactor selection backup / restore Avoid making the selection backup a state of the selection; rather the caller needs to keep this data. This allows us to correctly "roll back" the selection of nested extension calls, and we can also avoid spamming selection cleared events during document rebase after an extension. --- src/actions/actions-selection.cpp | 6 +- src/document.cpp | 5 +- src/document.h | 2 +- src/extension/execution-env.cpp | 14 +-- src/extension/execution-env.h | 7 +- src/extension/implementation/script.cpp | 33 ++++++- src/extension/implementation/script.h | 3 + src/selection.cpp | 116 +++++++++++------------- src/selection.h | 44 ++++++--- src/ui/tools/pages-tool.cpp | 9 +- src/ui/tools/pages-tool.h | 6 +- 11 files changed, 146 insertions(+), 99 deletions(-) diff --git a/src/actions/actions-selection.cpp b/src/actions/actions-selection.cpp index 133b0f52c8..f24895dfce 100644 --- a/src/actions/actions-selection.cpp +++ b/src/actions/actions-selection.cpp @@ -241,7 +241,7 @@ selection_set_backup(InkscapeApplication* app) return; } - selection->setBackup(); + // selection->setBackup(); } void @@ -253,7 +253,7 @@ selection_restore_backup(InkscapeApplication* app) return; } - selection->restoreBackup(); + // selection->restoreBackup(); } void @@ -265,7 +265,7 @@ selection_empty_backup(InkscapeApplication* app) return; } - selection->emptyBackup(); + // selection->emptyBackup(); } const Glib::ustring SECTION = NC_("Action Section", "Select"); diff --git a/src/document.cpp b/src/document.cpp index 4a2ce27446..9c32656016 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -2272,9 +2272,10 @@ sigc::connection SPDocument::connectResourcesChanged(gchar const *key, return resources_changed_signals[q].connect(slot); } -sigc::connection -SPDocument::connectReconstructionStart(SPDocument::ReconstructionStart::slot_type slot) +sigc::connection SPDocument::connectReconstructionStart(SPDocument::ReconstructionStart::slot_type slot, bool first) { + if (first) + return _reconstruction_start_signal.connect_first(slot); return _reconstruction_start_signal.connect(slot); } diff --git a/src/document.h b/src/document.h index ec65ea5f06..6e04c94e65 100644 --- a/src/document.h +++ b/src/document.h @@ -497,7 +497,7 @@ public: sigc::connection connectBeforeCommit(BeforeCommitSignal::slot_type slot); sigc::connection connectIdChanged(const char *id, IDChangedSignal::slot_type slot); sigc::connection connectResourcesChanged(char const *key, SPDocument::ResourcesChangedSignal::slot_type slot); - sigc::connection connectReconstructionStart(ReconstructionStart::slot_type slot); + sigc::connection connectReconstructionStart(ReconstructionStart::slot_type slot, bool first = false); sigc::connection connectReconstructionFinish(ReconstructionFinish::slot_type slot); sigc::connection connectSavedOrModified(sigc::slot &&slot); diff --git a/src/extension/execution-env.cpp b/src/extension/execution-env.cpp index 7fc54c7a81..71d210ef0e 100644 --- a/src/extension/execution-env.cpp +++ b/src/extension/execution-env.cpp @@ -174,10 +174,9 @@ ExecutionEnv::commit () { void ExecutionEnv::reselect () { - if(_desktop) { - Inkscape::Selection *selection = _desktop->getSelection(); - if (selection) { - selection->restoreBackup(); + if (_desktop && _selectionState) { + if (auto selection = _desktop->getSelection()) { + selection->setState(*_selectionState); } } return; @@ -192,7 +191,8 @@ ExecutionEnv::run () { createWorkingDialog(); } auto selection = _desktop->getSelection(); - selection->setBackup(); + // Save selection state + _selectionState = std::make_unique(selection->getState()); if (_show_working) { _desktop->setWaitingCursor(); } @@ -200,7 +200,9 @@ ExecutionEnv::run () { if (_show_working) { _desktop->clearWaitingCursor(); } - selection->restoreBackup(); + // Restore selection state + selection->setState(*_selectionState); + _selectionState.reset(); } else { _effect->get_imp()->effect(_effect, this, _document); } diff --git a/src/extension/execution-env.h b/src/extension/execution-env.h index deefbd0ca8..9d62b3cf55 100644 --- a/src/extension/execution-env.h +++ b/src/extension/execution-env.h @@ -13,8 +13,10 @@ #include #include - #include +#include + +#include "selection.h" class SPDesktop; class SPDocument; @@ -53,6 +55,9 @@ private: /** \brief A document cache if we were passed one. */ Implementation::ImplementationDocumentCache * _docCache; + /** \brief Saved selection state before running the effect. */ + std::unique_ptr _selectionState; + /** \brief The effect that we're executing in this context. */ Effect * _effect; diff --git a/src/extension/implementation/script.cpp b/src/extension/implementation/script.cpp index eca5764968..2197c5d2dc 100644 --- a/src/extension/implementation/script.cpp +++ b/src/extension/implementation/script.cpp @@ -578,10 +578,22 @@ void Script::effect(Inkscape::Extension::Effect *module, ExecutionEnv *execution std::list params; if (desktop) { - Inkscape::Selection * selection = desktop->getSelection(); - if (selection) { - params = selection->params; - selection->clear(); + if (auto selection = desktop->getSelection()) { + // Get current selection state + auto state = selection->getState(); + + // Add selected object IDs + for (auto const &id : state.selected_ids) { + std::string selected_id = "--id="; + selected_id += id; + params.push_back(std::move(selected_id)); + } + + // Add selected nodes + for (auto const &node : state.selected_nodes) { + params.push_back(Glib::ustring::compose("--selected-nodes=%1:%2:%3", node.path_id, node.subpath_index, + node.node_index)); + } } } _change_extension(module, executionEnv, desktop->getDocument(), params, module->ignore_stderr, module->pipe_diffs); @@ -935,12 +947,22 @@ void Script::PreviewObserver::connect(SPDesktop const *desktop, SPDocument *docu auto selection = desktop->getSelection(); _select_changed = selection->connectChanged([this](Inkscape::Selection *selection) { selectionChanged(selection); }); + // We don't want to spam deselect / select events + // while document reconstruction is ongoing. + // The selection is restored after the reconstruction, so + // we will emit an event there anyway. + _reconstruction_start_connection = + document->connectReconstructionStart([this]() { _pause_select_events = true; }, true); + _reconstruction_finish_connection = + document->connectReconstructionFinish([this]() { _pause_select_events = false; }); } void Script::PreviewObserver::disconnect(SPDocument *document) { document->removeUndoObserver(*this); _select_changed.disconnect(); + _reconstruction_start_connection.disconnect(); + _reconstruction_finish_connection.disconnect(); } void Script::PreviewObserver::createAndSendEvent( @@ -962,6 +984,9 @@ void Script::PreviewObserver::createAndSendEvent( void Script::PreviewObserver::selectionChanged(Inkscape::Selection *selection) { + if (_pause_select_events) { + return; + } createAndSendEvent([&](Inkscape::XML::Document *doc, Inkscape::XML::Node *event_node) { event_node->setAttribute("type", "updateSelection"); for (auto objsel : selection->objects()) { diff --git a/src/extension/implementation/script.h b/src/extension/implementation/script.h index b1b8dbc0e6..cf5bbd7e50 100644 --- a/src/extension/implementation/script.h +++ b/src/extension/implementation/script.h @@ -138,7 +138,10 @@ private: std::function const &eventPopulator); sigc::connection _select_changed; + sigc::connection _reconstruction_start_connection; + sigc::connection _reconstruction_finish_connection; Glib::RefPtr _channel; + bool _pause_select_events = false; }; int execute(std::list const &in_command, std::list const &in_params, diff --git a/src/selection.cpp b/src/selection.cpp index efd45101df..d4db273423 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -291,78 +291,64 @@ void Selection::_releaseSignals(SPObject *object) { _modified_connections.erase(object); } -void -Selection::emptyBackup(){ - _selected_ids.clear(); - _seldata.clear(); - params.clear(); -} - -void -Selection::setBackup () +SelectionState Selection::getState() { - SPDesktop *desktop = this->desktop(); - Inkscape::UI::Tools::NodeTool *tool = nullptr; - if (desktop) { - if (auto nt = dynamic_cast(desktop->getTool())) { - tool = nt; - } - } - - emptyBackup(); + SelectionState state; + // Get IDs of selected objects for (auto const * const item : items()) { - auto id = item->getId(); - if (!id) continue; - - std::string selected_id; - selected_id += "--id="; - selected_id += id; - params.push_back(std::move(selected_id)); - - _selected_ids.emplace_back(std::move(id)); + if (auto id = item->getId()) { + state.selected_ids.emplace_back(id); + } } - if (!tool) return; - - for (auto const point : tool->_selected_nodes->_points_list) { - auto const node = dynamic_cast(point); - if (!node) continue; + // If node tool is active, get selected nodes + if (SPDesktop *desktop = this->desktop()) { + if (auto tool = dynamic_cast(desktop->getTool())) { + for (auto const point : tool->_selected_nodes->_points_list) { + auto const node = dynamic_cast(point); + if (!node) + continue; + + auto const &nodeList = node->nodeList(); + auto const &subpathList = nodeList.subpathList(); + + // Find subpath index + int sp = 0; + bool found_sp = false; + for (auto i = subpathList.begin(), e = subpathList.end(); i != e; ++i, ++sp) { + if (&**i == &nodeList) { + found_sp = true; + break; + } + } - auto const &nodeList = node->nodeList(); - auto const &subpathList = nodeList.subpathList(); + // Find node index + int nl = 0; + bool found_nl = false; + for (auto j = nodeList.begin(), e = nodeList.end(); j != e; ++j, ++nl) { + if (&*j == node) { + found_nl = true; + break; + } + } - int sp = 0; - bool found_sp = false; - for (auto i = subpathList.begin(), e = subpathList.end(); i != e; ++i, ++sp) { - if (&**i == &nodeList) { - found_sp = true; - break; - } - } + if (!(found_nl && found_sp)) { + g_warning("Something went wrong while trying to get node info. Please report a bug."); + continue; + } - int nl = 0; - bool found_nl = false; - for (auto j = nodeList.begin(), e = nodeList.end(); j != e; ++j, ++nl) { - if (&*j == node){ - found_nl = true; - break; + if (auto id = subpathList.pm().item()->getId()) { + state.selected_nodes.emplace_back(id, sp, nl); + } } } - - if (!(found_nl && found_sp)) { - g_warning("Something went wrong while trying to pass selected nodes to extension. Please report a bug."); - return; - } - - auto id = subpathList.pm().item()->getId(); - params.push_back(Glib::ustring::compose("--selected-nodes=%1:%2:%3", id, sp, nl)); - _seldata.emplace_back(std::move(id), std::make_pair(sp, nl)); } + + return state; } -void -Selection::restoreBackup() +void Selection::setState(SelectionState const &state) { SPDesktop *desktop = this->desktop(); SPDocument *document = SP_ACTIVE_DOCUMENT; @@ -376,13 +362,14 @@ Selection::restoreBackup() // update selection std::vector new_selection; - for (auto const &selected_id : _selected_ids) { + for (auto const &selected_id : state.selected_ids) { auto const item = cast(document->getObjectById(selected_id.c_str())); if (item && !defs->isAncestorOf(item)) { new_selection.push_back(item); } } - clear(); + if (size()) + clear(); add(new_selection.begin(), new_selection.end()); new_selection.clear(); @@ -398,14 +385,15 @@ Selection::restoreBackup() if (!node) return; auto const &sp = node->nodeList().subpathList(); - for (auto & l : _seldata) { + for (auto const &node_state : state.selected_nodes) { int sp_count = 0; for (auto j = sp.begin(); j != sp.end(); ++j, ++sp_count) { - if (sp_count != l.second.first) continue; + if (sp_count != node_state.subpath_index) + continue; int nt_count = 0; for (auto k = (*j)->begin(); k != (*j)->end(); ++k, ++nt_count) { - if (nt_count == l.second.second) { + if (nt_count == node_state.node_index) { cps->insert(k.ptr()); break; } diff --git a/src/selection.h b/src/selection.h index 28e1b92c8c..2c74f4ad7b 100644 --- a/src/selection.h +++ b/src/selection.h @@ -34,6 +34,31 @@ namespace XML { class Node; } // namespace XML +/** + * Represents a selected node in a path + */ +struct PathNodeState +{ + std::string path_id; // ID of the path containing the node + int subpath_index; // Index of the subpath + int node_index; // Index of the node within the subpath + + PathNodeState(std::string id, int sp, int n) + : path_id(std::move(id)) + , subpath_index(sp) + , node_index(n) + {} +}; + +/** + * Complete state of a selection, including selected objects and nodes + */ +struct SelectionState +{ + std::vector selected_ids; // IDs of selected objects + std::vector selected_nodes; // Selected path nodes (when node tool is active) +}; + /** * The set of selected SPObjects for a given document and layer model. * @@ -216,21 +241,14 @@ public: } /** - * Set a backup of current selection and store it also to be command line readable by extension system - */ - void setBackup(); - /** - * Clear backup of current selection + * Returns the current selection state including selected objects and nodes */ - void emptyBackup(); - /** - * Restore a selection from a existing backup - */ - void restoreBackup(); + SelectionState getState(); + /** - * Here store a paramlist when set backup + * Restores a selection state previously obtained from getState() */ - std::list params; + void setState(SelectionState const &state); /** * Decide if the selection changing should change the layer and page selection too @@ -262,8 +280,6 @@ private: unsigned _idle = 0; bool _change_layer = true; bool _change_page = true; - std::vector>> _seldata; - std::vector _selected_ids; std::unordered_map _modified_connections; sigc::scoped_connection _context_release_connection; diff --git a/src/ui/tools/pages-tool.cpp b/src/ui/tools/pages-tool.cpp index 8fa18e64d8..b4bca2ac3e 100644 --- a/src/ui/tools/pages-tool.cpp +++ b/src/ui/tools/pages-tool.cpp @@ -44,8 +44,8 @@ namespace Inkscape::UI::Tools { PagesTool::PagesTool(SPDesktop *desktop) : ToolBase(desktop, "/tools/pages", "select.svg") { - // Stash the regular object selection so we don't modify them in base-tools root handler. - desktop->getSelection()->setBackup(); + // Save selection state and clear selection before using the tool + _selection_state = std::make_unique(desktop->getSelection()->getState()); desktop->getSelection()->clear(); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); @@ -433,7 +433,10 @@ void PagesTool::menu_popup(CanvasEvent const &event, SPObject *obj) void PagesTool::switching_away(std::string const &) { - _desktop->getSelection()->restoreBackup(); + if (_selection_state) { + _desktop->getSelection()->setState(*_selection_state); + _selection_state.reset(); + } } /** diff --git a/src/ui/tools/pages-tool.h b/src/ui/tools/pages-tool.h index 78c3fba211..a96f0f8b3b 100644 --- a/src/ui/tools/pages-tool.h +++ b/src/ui/tools/pages-tool.h @@ -13,9 +13,12 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include "ui/tools/tool-base.h" #include <2geom/rect.h> +#include + #include "display/control/canvas-item-ptr.h" +#include "selection.h" +#include "ui/tools/tool-base.h" #define SP_PAGES_CONTEXT(obj) (dynamic_cast((Inkscape::UI::Tools::ToolBase *)obj)) #define SP_IS_PAGES_CONTEXT(obj) \ @@ -91,6 +94,7 @@ private: CanvasItemPtr drag_group; std::vector drag_shapes; std::vector _bbox_points; + std::unique_ptr _selection_state; static Geom::Point middleOfSide(int side, const Geom::Rect &rect); }; -- GitLab From 786f001f145023402e8dd14b76cdd57875c50cae Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Wed, 5 Mar 2025 20:54:42 +0100 Subject: [PATCH 07/11] Remove selection backup actions --- src/actions/actions-selection.cpp | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/src/actions/actions-selection.cpp b/src/actions/actions-selection.cpp index f24895dfce..eaa021c001 100644 --- a/src/actions/actions-selection.cpp +++ b/src/actions/actions-selection.cpp @@ -232,42 +232,6 @@ select_list(InkscapeApplication* app) } } -void -selection_set_backup(InkscapeApplication* app) -{ - SPDocument* document = nullptr; - Inkscape::Selection* selection = nullptr; - if (!get_document_and_selection(app, &document, &selection)) { - return; - } - - // selection->setBackup(); -} - -void -selection_restore_backup(InkscapeApplication* app) -{ - SPDocument* document = nullptr; - Inkscape::Selection* selection = nullptr; - if (!get_document_and_selection(app, &document, &selection)) { - return; - } - - // selection->restoreBackup(); -} - -void -selection_empty_backup(InkscapeApplication* app) -{ - SPDocument* document = nullptr; - Inkscape::Selection* selection = nullptr; - if (!get_document_and_selection(app, &document, &selection)) { - return; - } - - // selection->emptyBackup(); -} - const Glib::ustring SECTION = NC_("Action Section", "Select"); std::vector> raw_data_selection = @@ -283,9 +247,6 @@ std::vector> raw_data_selection = {"app.select-by-selector", N_("Select by Selector"), SECTION, N_("Select by CSS selector")}, {"app.select-all", N_("Select All Objects"), SECTION, N_("Select all; options: 'all' (every object including groups), 'layers', 'no-layers' (top level objects in layers), 'groups' (all groups including layers), 'no-groups' (all objects other than groups and layers, default)")}, {"app.select-list", N_("List Selection"), SECTION, N_("Print a list of objects in current selection")}, - {"app.selection-set-backup", N_("Set selection backup"), SECTION, N_("Set backup of current selection of objects or nodes")}, - {"app.selection-restore-backup", N_("Restore selection backup"), SECTION, N_("Restore backup of stored selection of objects or nodes")}, - {"app.selection-empty-backup", N_("Empty selection backup"), SECTION, N_("Empty stored backup of selection of objects or nodes")}, // clang-format on }; @@ -306,9 +267,6 @@ add_actions_selection(InkscapeApplication* app) gapp->add_action_radio_string( "select-all", sigc::bind(sigc::ptr_fun(&select_all), app), "null"); gapp->add_action_radio_string( "select-invert", sigc::bind(sigc::ptr_fun(&select_invert), app), "null"); gapp->add_action( "select-list", sigc::bind(sigc::ptr_fun(&select_list), app) ); - gapp->add_action( "selection-set-backup", sigc::bind(sigc::ptr_fun(&selection_set_backup), app) ); - gapp->add_action( "selection-restore-backup", sigc::bind(sigc::ptr_fun(&selection_restore_backup), app) ); - gapp->add_action( "selection-empty-backup", sigc::bind(sigc::ptr_fun(&selection_empty_backup), app) ); // clang-format on app->get_action_extra_data().add_data(raw_data_selection); -- GitLab From c22806c45b10760e003fdf826fea76b6fe953126 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Fri, 7 Mar 2025 08:07:59 +0000 Subject: [PATCH 08/11] Add missing tags to cursor svg files (dash theme) --- share/icons/Dash/cursors/calligraphy.svg | 3 ++- share/icons/Dash/cursors/flood.svg | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/share/icons/Dash/cursors/calligraphy.svg b/share/icons/Dash/cursors/calligraphy.svg index c09b08eccc..036f5b08a4 100644 --- a/share/icons/Dash/cursors/calligraphy.svg +++ b/share/icons/Dash/cursors/calligraphy.svg @@ -13,4 +13,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/share/icons/Dash/cursors/flood.svg b/share/icons/Dash/cursors/flood.svg index 2f8e89caa2..47a10e45c0 100644 --- a/share/icons/Dash/cursors/flood.svg +++ b/share/icons/Dash/cursors/flood.svg @@ -19,7 +19,5 @@ - - - - + + \ No newline at end of file -- GitLab From 61f0b71f7e06266bf3550689d74a0b3702958783 Mon Sep 17 00:00:00 2001 From: ftomara Date: Fri, 14 Mar 2025 04:14:39 +0200 Subject: [PATCH 09/11] added rectangle lock feature and synced it with knots --- .gitlab-ci.yml | 6 +- CMakeScripts/DefineDependsandFlags.cmake | 1 - share/ui/toolbar-rect.ui | 28 +++++++++ src/object/sp-rect.cpp | 66 +++++++++++++++++++- src/object/sp-rect.h | 18 ++++++ src/ui/shape-editor-knotholders.cpp | 45 +++++++++++++- src/ui/toolbar/rect-toolbar.cpp | 79 ++++++++++++++++++++++-- src/ui/toolbar/rect-toolbar.h | 16 ++++- 8 files changed, 246 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index abd9163df6..4f7d15be5e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -89,9 +89,9 @@ inkscape:linux: <<: *ccache_init script: - *cmake - - make -j2 install - - make -j2 tests - - make -j2 unit_tests + - make -j3 install + - make -j3 tests + - make -j3 unit_tests #- cpack -G DEB - rm -rf src _CPack_Packages # exclude from artifacts - cd .. diff --git a/CMakeScripts/DefineDependsandFlags.cmake b/CMakeScripts/DefineDependsandFlags.cmake index 744e9787c3..0425307d62 100644 --- a/CMakeScripts/DefineDependsandFlags.cmake +++ b/CMakeScripts/DefineDependsandFlags.cmake @@ -45,7 +45,6 @@ list(APPEND INKSCAPE_CXX_FLAGS "-Werror=ignored-qualifiers") # e.g.: const in list(APPEND INKSCAPE_CXX_FLAGS "-Werror=return-type") # non-void functions that don't return a value list(APPEND INKSCAPE_CXX_FLAGS "-Wno-switch") # See !849 for discussion list(APPEND INKSCAPE_CXX_FLAGS "-Wmisleading-indentation") -list(APPEND INKSCAPE_CXX_FLAGS_DEBUG "-Og") # -Og for _FORTIFY_SOURCE. One could add -Weffc++ here to see approx. 6000 warnings list(APPEND INKSCAPE_CXX_FLAGS_DEBUG "-Wcomment") list(APPEND INKSCAPE_CXX_FLAGS_DEBUG "-Wunused-function") list(APPEND INKSCAPE_CXX_FLAGS_DEBUG "-Wunused-variable") diff --git a/share/ui/toolbar-rect.ui b/share/ui/toolbar-rect.ui index 69902e957e..8c9b50f6e3 100644 --- a/share/ui/toolbar-rect.ui +++ b/share/ui/toolbar-rect.ui @@ -52,6 +52,20 @@ + + + center + True + True + False + Lock width and height proportions + + + object-unlocked + + + + Height of rectangle @@ -96,6 +110,20 @@ + + + center + True + True + False + Lock Rx and Ry proportions + + + object-unlocked + + + + Vertical radius of rounded corners diff --git a/src/object/sp-rect.cpp b/src/object/sp-rect.cpp index def363af99..d9062436c1 100644 --- a/src/object/sp-rect.cpp +++ b/src/object/sp-rect.cpp @@ -67,6 +67,12 @@ void SPRect::build(SPDocument* doc, Inkscape::XML::Node* repr) { this->readAttr(SPAttr::RX); this->readAttr(SPAttr::RY); + // Read custom attributes for lock state and aspect ratio + this->readAttr("inkscape:lock-wh"); + this->readAttr("inkscape:aspect-ratio-wh"); + this->readAttr("inkscape:lock-rxy"); + this->readAttr("inkscape:aspect-ratio-rxy"); + #ifdef OBJECT_TRACE objectTrace( "SPRect::build", false ); #endif @@ -132,7 +138,26 @@ void SPRect::set(SPAttr key, gchar const *value) { this->ry.update( em, ex, h ); this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; - + // Custom attributes + case SPAttr::INVALID: // Handle custom attributes + if (strcmp(sp_attribute_name(key), "inkscape:lock-wh") == 0) { + lock_wh = value && strcmp(value, "true") == 0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else if (strcmp(sp_attribute_name(key), "inkscape:aspect-ratio-wh") == 0) { + aspect_ratio_wh = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_wh <= 0) aspect_ratio_wh = 1.0; // Prevent division by zero + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else if (strcmp(sp_attribute_name(key), "inkscape:lock-rxy") == 0) { + lock_rxy = value && strcmp(value, "true") == 0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else if (strcmp(sp_attribute_name(key), "inkscape:aspect-ratio-rxy") == 0) { + aspect_ratio_rxy = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_rxy <= 0) aspect_ratio_rxy = 1.0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + SPShape::set(key, value); + } + break; default: SPShape::set(key, value); break; @@ -223,6 +248,13 @@ Inkscape::XML::Node * SPRect::write(Inkscape::XML::Document *xml_doc, Inkscape:: repr->setAttributeSvgLength("x", this->x); repr->setAttributeSvgLength("y", this->y); + + // Write custom attributes + repr->setAttribute("inkscape:lock-wh", lock_wh ? "true" : "false"); + repr->setAttribute("inkscape:aspect-ratio-wh", Glib::ustring::format(aspect_ratio_wh)); + repr->setAttribute("inkscape:lock-rxy", lock_rxy ? "true" : "false"); + repr->setAttribute("inkscape:aspect-ratio-rxy", Glib::ustring::format(aspect_ratio_rxy)); + // write d= if (type == SP_GENERIC_PATH) { set_rect_path_attribute(repr); // include set_shape() @@ -647,6 +679,38 @@ void SPRect::convert_to_guides() const { sp_guide_pt_pairs_to_guides(this->document, pts); } +void SPRect::setLockWh(bool lock) { + lock_wh = lock; +} + +bool SPRect::getLockWh() const { + return lock_wh; +} + +void SPRect::setLockRxy(bool lock) { + lock_rxy = lock; +} + +bool SPRect::getLockRxy() const { + return lock_rxy; +} + +void SPRect::setAspectRatioWh(double ratio) { + aspect_ratio_wh = ratio; +} + +double SPRect::getAspectRatioWh() const { + return aspect_ratio_wh; +} + +void SPRect::setAspectRatioRxy(double ratio) { + aspect_ratio_rxy = ratio; +} + +double SPRect::getAspectRatioRxy() const { + return aspect_ratio_rxy; +} + /* Local Variables: mode:c++ diff --git a/src/object/sp-rect.h b/src/object/sp-rect.h index f157e81e10..aa4fe5e260 100644 --- a/src/object/sp-rect.h +++ b/src/object/sp-rect.h @@ -54,6 +54,20 @@ public: double getVisibleHeight() const; void setVisibleHeight(double ry); + bool getLockWh() const; + void setLockWh(bool lock); + + bool getLockRxy() const; + void setLockRxy(bool lock); + + double getAspectRatioWh() const; + void setAspectRatioWh(double ratio); + + double getAspectRatioRxy() const; + void setAspectRatioRxy(double ratio); + + + void compensateRxRy(Geom::Affine xform); void build(SPDocument* doc, Inkscape::XML::Node* repr) override; @@ -79,6 +93,10 @@ public: SVGLength height; SVGLength rx; SVGLength ry; + bool lock_wh=false; + bool lock_rxy=false; + double aspect_ratio_wh=0.0; + double aspect_ratio_rxy=0.0; private: static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp index 77cb213333..9b6b6595d5 100644 --- a/src/ui/shape-editor-knotholders.cpp +++ b/src/ui/shape-editor-knotholders.cpp @@ -245,7 +245,15 @@ RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*orig if (state & GDK_CONTROL_MASK) { gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; rect->rx = rect->ry = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, temp); - } else { + }else if(rect->getLockRxy()&& rect->ry._set) + { + double new_rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0); + rect->rx = new_rx; + rect->ry = new_rx * rect->getAspectRatioRxy(); + rect->ry = MIN(rect->ry.computed, rect->height.computed / 2.0); + + } + else { rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0); } @@ -295,7 +303,14 @@ RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*orig // resulting in a perfect circle (and not an ellipse) gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; rect->rx = rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, temp); - } else { + }else if(rect->getLockRxy()&& rect->rx._set) + { + double new_ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, rect->height.computed / 2.0); + rect->ry = new_ry; + rect->rx = new_ry / rect->getAspectRatioRxy(); + rect->rx = MIN(rect->rx.computed, rect->width.computed / 2.0); + } + else { if (!rect->rx._set || rect->rx.computed == 0) { rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, @@ -357,6 +372,8 @@ RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &or g_assert(rect != nullptr); Geom::Point s = p; + double old_width = rect->width.computed; + double old_height = rect->height.computed; if (state & GDK_CONTROL_MASK) { // original width/height when drag started @@ -412,7 +429,29 @@ RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &or } - } else { + } else if (rect->getLockWh()){ + + s = snap_knot_position(p, state); + double new_width = MAX(s[Geom::X] - rect->x.computed, 0); + double new_height = MAX(s[Geom::Y] - rect->y.computed, 0); + + + double aspect = rect->getAspectRatioWh(); // height / width + bool width_changed = fabs(new_width - old_width) > 1e-6; + bool height_changed = fabs(new_height - old_height) > 1e-6; + + if (width_changed && !height_changed) { + new_height = new_width * aspect; + } else if (height_changed && !width_changed) { + new_width = new_height / aspect; + } else if (width_changed && height_changed) { + new_height = new_width * aspect; // Prioritize width for corner drag + } + + rect->width = new_width; + rect->height = new_height; + } + else { // move freely s = snap_knot_position(p, state); rect->width = MAX(s[Geom::X] - rect->x.computed, 0); diff --git a/src/ui/toolbar/rect-toolbar.cpp b/src/ui/toolbar/rect-toolbar.cpp index 4887e6078d..872cb1965c 100644 --- a/src/ui/toolbar/rect-toolbar.cpp +++ b/src/ui/toolbar/rect-toolbar.cpp @@ -27,12 +27,16 @@ #include "rect-toolbar.h" +#include + #include #include #include -#include +#include #include #include +#include +#include #include "desktop.h" #include "document-undo.h" @@ -47,6 +51,7 @@ #include "ui/widget/spinbutton.h" #include "ui/widget/unit-tracker.h" #include "widgets/widget-sizes.h" +#include "preferences.h" using Inkscape::UI::Widget::UnitTracker; using Inkscape::DocumentUndo; @@ -85,12 +90,19 @@ RectToolbar::RectToolbar(Glib::RefPtr const &builder) , _height_item{UI::get_derived_widget(builder, "_height_item", "height", &SPRect::getVisibleHeight, &SPRect::setVisibleHeight)} , _rx_item{UI::get_derived_widget(builder, "_rx_item", "rx", &SPRect::getVisibleRx, &SPRect::setVisibleRx)} , _ry_item{UI::get_derived_widget(builder, "_ry_item", "ry", &SPRect::getVisibleRy, &SPRect::setVisibleRy)} + , _lock_wh_button{get_widget(builder, "_lock_wh_button")} + , _lock_rxy_button{get_widget(builder, "_lock_rxy_button")} { + auto prefs = Inkscape::Preferences::get(); auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); get_widget(builder, "unit_menu_box").append(*unit_menu); _not_rounded.signal_clicked().connect([this] { _setDefaults(); }); + // Configure the lock buttons + _lock_wh_button.signal_toggled().connect([this] { toggle_lock_wh(); }); + _lock_rxy_button.signal_toggled().connect([this] { toggle_lock_rxy(); }); + for (auto sb : _getDerivedSpinButtons()) { auto const adj = sb->get_adjustment(); auto const path = Glib::ustring{"/tools/shapes/rect/"} + sb->name; @@ -213,20 +225,52 @@ void RectToolbar::_valueChanged(DerivedSpinButton &btn) auto guard = _blocker.block(); auto const adj = btn.get_adjustment(); + double value = Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px"); + // save the new value to preferences if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { auto const path = Glib::ustring{"/tools/shapes/rect/"} + btn.name; - Preferences::get()->setDouble(path, Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px")); + Preferences::get()->setDouble(path, value); } bool modified = false; for (auto item : _desktop->getSelection()->items()) { if (auto rect = cast(item)) { - if (adj->get_value() != 0) { - (rect->*btn.setter)(Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px")); + double new_value = value; + double paired_value = 0.0; + + if (&btn == &_width_item && rect->getLockWh()) { + paired_value = new_value * rect->getAspectRatioWh(); + _height_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleHeight(paired_value); + } else if (&btn == &_height_item && rect->getLockWh()) { + paired_value = new_value / rect->getAspectRatioWh(); + _width_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleWidth(paired_value); + } else if (&btn == &_rx_item && rect->getLockRxy()) { + paired_value = new_value * rect->getAspectRatioRxy(); + _ry_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleRy(paired_value); + } else if (&btn == &_ry_item && rect->getLockRxy()) { + paired_value = new_value / rect->getAspectRatioRxy(); + _rx_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleRx(paired_value); + } + + // Update the primary dimension + if (new_value != 0) { + (rect->*btn.setter)(new_value); } else { rect->removeAttribute(btn.name); } + + // Update aspect ratio after change + double w = rect->getVisibleWidth(); + double h = rect->getVisibleHeight(); + rect->setAspectRatioWh(h / (w != 0 ? w : 1.0)); + double rx = rect->getVisibleRx(); + double ry = rect->getVisibleRy(); + rect->setAspectRatioRxy(ry / (rx != 0 ? rx : 1.0)); modified = true; } } @@ -275,6 +319,10 @@ void RectToolbar::_selectionChanged(Selection *selection) if (_single) { _attachRepr(repr, rect); + _lock_wh_button.set_active(rect->getLockWh()); + _lock_rxy_button.set_active(rect->getLockRxy()); + _aspect_ratio_wh = rect->getAspectRatioWh(); + _aspect_ratio_rxy = rect->getAspectRatioRxy(); _queueUpdate(); } @@ -340,6 +388,29 @@ void RectToolbar::_update() _sensitivize(); } +void RectToolbar::toggle_lock_wh() { + bool active = _lock_wh_button.get_active(); + if (_single && _rect) { + _rect->setLockWh(active); + _lock_wh_button.set_image_from_icon_name(active ? "object-locked" : "object-unlocked"); + double w = _rect->getVisibleWidth(); + double h = _rect->getVisibleHeight(); + _rect->setAspectRatioWh(h / (w != 0 ? w : 1.0)); // Update aspect ratio when toggling + DocumentUndo::done(_desktop->getDocument(), _("Toggle rectangle lock"), INKSCAPE_ICON("draw-rectangle")); + } +} + +void RectToolbar::toggle_lock_rxy() { + bool active = _lock_rxy_button.get_active(); + if (_single && _rect) { + _rect->setLockRxy(active); + _lock_rxy_button.set_image_from_icon_name(active ? "object-locked" : "object-unlocked"); + double rx = _rect->getVisibleRx(); + double ry = _rect->getVisibleRy(); + _rect->setAspectRatioRxy(ry / (rx != 0 ? rx : 1.0)); + DocumentUndo::done(_desktop->getDocument(), _("Toggle rectangle lock"), INKSCAPE_ICON("draw-rectangle")); + } +} } // namespace Inkscape::UI::Toolbar /* diff --git a/src/ui/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h index 14d7d1f414..1296342cda 100644 --- a/src/ui/toolbar/rect-toolbar.h +++ b/src/ui/toolbar/rect-toolbar.h @@ -29,8 +29,9 @@ */ #include - +#include #include "toolbar.h" +#include "preferences.h" #include "ui/operation-blocker.h" #include "xml/node-observer.h" @@ -39,6 +40,7 @@ class Builder; class Button; class Label; class Adjustment; +class ToggleButton; } // namespace Gtk class SPRect; @@ -68,6 +70,8 @@ public: void setDesktop(SPDesktop *desktop) override; void setActiveUnit(Util::Unit const *unit) override; + bool getLockWH() const { return _lock_wh; } + bool getLockRXY() const { return _lock_rxy; } private: RectToolbar(Glib::RefPtr const &builder); @@ -85,6 +89,16 @@ private: auto _getDerivedSpinButtons() const { return std::to_array({&_rx_item, &_ry_item, &_width_item, &_height_item}); } void _valueChanged(DerivedSpinButton &btn); + // Add lock button and state + Gtk::ToggleButton &_lock_wh_button; // New toggle button for locking width/height + Gtk::ToggleButton &_lock_rxy_button; // New toggle button for locking rx/ry + bool _lock_wh = false; + bool _lock_rxy = false; + double _aspect_ratio_wh=0.0; + double _aspect_ratio_rxy=0.0; + void toggle_lock_wh(); + void toggle_lock_rxy(); + XML::Node *_repr = nullptr; SPRect *_rect = nullptr; void _attachRepr(XML::Node *repr, SPRect *rect); -- GitLab From bfb70cbfac363af40b499af44e5207bff9ba0c0e Mon Sep 17 00:00:00 2001 From: Ft Omara Date: Fri, 14 Mar 2025 12:24:13 +0000 Subject: [PATCH 10/11] Edit .gitlab-ci.yml , made test:linux 3h timeout --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f7d15be5e..2e9d4762db 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -99,8 +99,6 @@ inkscape:linux: expire_in: 1 year paths: - build/ - timeout: 3h - appimage:linux: stage: build @@ -213,6 +211,7 @@ inkscape:windows: #tests, always run after building test:linux: stage: test + timeout: 3h rules: - *do_not_run_for_schedules - *run_otherwise -- GitLab From 9bd521b89596ea8edd45fb9a5866711aa38e22e9 Mon Sep 17 00:00:00 2001 From: ftomara Date: Sat, 15 Mar 2025 14:05:19 +0200 Subject: [PATCH 11/11] made tooltip translatble and fixed propertey issue , registerd attributes in attributes.h/.cpp files --- .gitignore | 5 +++ share/ui/toolbar-rect.ui | 4 +-- src/attributes.cpp | 6 ++++ src/attributes.h | 10 +++++- src/object/sp-rect.cpp | 66 ++++++++++++++++++++++------------------ src/object/sp-rect.h | 5 +-- testfiles/CMakeLists.txt | 1 + 7 files changed, 63 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 100b5bd725..99808bb1b5 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,8 @@ tags # YCM code completer .ycm_* + +# Ignore build and install files +install-prefix/ +bin/ +lib/ \ No newline at end of file diff --git a/share/ui/toolbar-rect.ui b/share/ui/toolbar-rect.ui index 8c9b50f6e3..9bea068b08 100644 --- a/share/ui/toolbar-rect.ui +++ b/share/ui/toolbar-rect.ui @@ -58,7 +58,7 @@ True True False - Lock width and height proportions + Lock width and height proportions object-unlocked @@ -116,7 +116,7 @@ True True False - Lock Rx and Ry proportions + Lock Rx and Ry proportions object-unlocked diff --git a/src/attributes.cpp b/src/attributes.cpp index 54cd18c428..99525fe622 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -598,6 +598,12 @@ static SPStyleProp const props[] = { // named view settings {SPAttr::INKSCAPE_ORIGIN_CORRECTION, "origin-correction"}, {SPAttr::INKSCAPE_Y_AXIS_DOWN, "y-axis-down"}, + + // rectangle lock aspect ratio state + {SPAttr::INKSCAPE_LOCK_WH, "inkscape:lock-wh"}, + {SPAttr::INKSCAPE_ASPECT_RATIO_WH, "inkscape:aspect-ratio-wh"}, + {SPAttr::INKSCAPE_LOCK_RXY, "inkscape:lock-rxy"}, + {SPAttr::INKSCAPE_ASPECT_RATIO_RXY, "inkscape:aspect-ratio-rxy"}, }; #define n_attrs (sizeof(props) / sizeof(props[0])) diff --git a/src/attributes.h b/src/attributes.h index 85eb0b8617..05b56ec4f9 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -600,8 +600,16 @@ enum class SPAttr { INKSCAPE_ORIGIN_CORRECTION, INKSCAPE_Y_AXIS_DOWN, + + // rectangle lock aspect ratio state + INKSCAPE_LOCK_WH, + INKSCAPE_ASPECT_RATIO_WH, + INKSCAPE_LOCK_RXY, + INKSCAPE_ASPECT_RATIO_RXY, + // sentinel - SPAttr_SIZE + SPAttr_SIZE, + }; /** diff --git a/src/object/sp-rect.cpp b/src/object/sp-rect.cpp index d9062436c1..c6dfca4478 100644 --- a/src/object/sp-rect.cpp +++ b/src/object/sp-rect.cpp @@ -32,7 +32,7 @@ //#define OBJECT_TRACE SPRect::SPRect() : SPShape() - ,type(SP_GENERIC_RECT_UNDEFINED) + ,type(SP_GENERIC_RECT_UNDEFINED), lock_wh(false), lock_rxy(false) { } @@ -68,10 +68,10 @@ void SPRect::build(SPDocument* doc, Inkscape::XML::Node* repr) { this->readAttr(SPAttr::RY); // Read custom attributes for lock state and aspect ratio - this->readAttr("inkscape:lock-wh"); - this->readAttr("inkscape:aspect-ratio-wh"); - this->readAttr("inkscape:lock-rxy"); - this->readAttr("inkscape:aspect-ratio-rxy"); + this->readAttr(SPAttr::INKSCAPE_LOCK_WH); + this->readAttr(SPAttr::INKSCAPE_ASPECT_RATIO_WH); + this->readAttr(SPAttr::INKSCAPE_LOCK_RXY); + this->readAttr(SPAttr::INKSCAPE_ASPECT_RATIO_RXY); #ifdef OBJECT_TRACE objectTrace( "SPRect::build", false ); @@ -138,26 +138,24 @@ void SPRect::set(SPAttr key, gchar const *value) { this->ry.update( em, ex, h ); this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; - // Custom attributes - case SPAttr::INVALID: // Handle custom attributes - if (strcmp(sp_attribute_name(key), "inkscape:lock-wh") == 0) { - lock_wh = value && strcmp(value, "true") == 0; - requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); - } else if (strcmp(sp_attribute_name(key), "inkscape:aspect-ratio-wh") == 0) { - aspect_ratio_wh = value ? g_ascii_strtod(value, nullptr) : 1.0; - if (aspect_ratio_wh <= 0) aspect_ratio_wh = 1.0; // Prevent division by zero - requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); - } else if (strcmp(sp_attribute_name(key), "inkscape:lock-rxy") == 0) { - lock_rxy = value && strcmp(value, "true") == 0; - requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); - } else if (strcmp(sp_attribute_name(key), "inkscape:aspect-ratio-rxy") == 0) { - aspect_ratio_rxy = value ? g_ascii_strtod(value, nullptr) : 1.0; - if (aspect_ratio_rxy <= 0) aspect_ratio_rxy = 1.0; - requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); - } else { - SPShape::set(key, value); - } - break; + case SPAttr::INKSCAPE_LOCK_WH: + lock_wh.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_ASPECT_RATIO_WH: + aspect_ratio_wh = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_wh <= 0) aspect_ratio_wh = 1.0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_LOCK_RXY: + lock_rxy.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_ASPECT_RATIO_RXY: + aspect_ratio_rxy = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_rxy <= 0) aspect_ratio_rxy = 1.0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; default: SPShape::set(key, value); break; @@ -250,10 +248,20 @@ Inkscape::XML::Node * SPRect::write(Inkscape::XML::Document *xml_doc, Inkscape:: repr->setAttributeSvgLength("y", this->y); // Write custom attributes - repr->setAttribute("inkscape:lock-wh", lock_wh ? "true" : "false"); - repr->setAttribute("inkscape:aspect-ratio-wh", Glib::ustring::format(aspect_ratio_wh)); - repr->setAttribute("inkscape:lock-rxy", lock_rxy ? "true" : "false"); - repr->setAttribute("inkscape:aspect-ratio-rxy", Glib::ustring::format(aspect_ratio_rxy)); + if (lock_wh) { + repr->setAttribute("inkscape:lock-wh", lock_wh ? "true" : "false"); + repr->setAttributeSvgDouble("inkscape:aspect-ratio-wh", this->aspect_ratio_wh); + } else { + repr->removeAttribute("inkscape:lock-wh"); + repr->removeAttribute("inkscape:aspect-ratio-wh"); + } + if (lock_rxy) { + repr->setAttribute("inkscape:lock-rxy", lock_rxy ? "true" : "false"); + repr->setAttributeSvgDouble("inkscape:aspect-ratio-rxy", this->aspect_ratio_rxy); + } else { + repr->removeAttribute("inkscape:lock-rxy"); + repr->removeAttribute("inkscape:aspect-ratio-rxy"); + } // write d= if (type == SP_GENERIC_PATH) { diff --git a/src/object/sp-rect.h b/src/object/sp-rect.h index aa4fe5e260..d3e4b2f763 100644 --- a/src/object/sp-rect.h +++ b/src/object/sp-rect.h @@ -18,6 +18,7 @@ #include <2geom/forward.h> #include "svg/svg-length.h" +#include "svg/svg-bool.h" #include "sp-shape.h" enum GenericRectType { @@ -93,8 +94,8 @@ public: SVGLength height; SVGLength rx; SVGLength ry; - bool lock_wh=false; - bool lock_rxy=false; + SVGBool lock_wh{false}; // Default false, unset + SVGBool lock_rxy{false}; // Default false, unset double aspect_ratio_wh=0.0; double aspect_ratio_rxy=0.0; diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index 0f25914853..f8283f629b 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -142,6 +142,7 @@ foreach(test_source ${TEST_SOURCES}) add_executable(${testname} src/${test_source}.cpp) target_include_directories(${testname} SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) target_link_libraries(${testname} cpp_test_static_library 2Geom::2geom) + set_target_properties(${testname} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") add_test(NAME ${testname} COMMAND ${testname}) set_tests_properties(${testname} PROPERTIES ENVIRONMENT "${INKSCAPE_TEST_PROFILE_DIR_ENV}/${testname};${CMAKE_CTEST_ENV}") add_dependencies(tests ${testname}) -- GitLab