diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp index 25521d651596a425883a3680dd65144253db3cb4..c701b61826907bbd4a9694d2e1823123945a81da 100644 --- a/src/ui/dialog/align-and-distribute.cpp +++ b/src/ui/dialog/align-and-distribute.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include "util/units.h" #include "actions/actions-tools.h" // Tool switching. #include "desktop.h" // Tool switching. @@ -32,6 +34,13 @@ #include "ui/builder-utils.h" #include "ui/dialog/dialog-base.h" // Tool switching. #include "ui/util.h" +#include "message-stack.h" // For status messages +#include "object/sp-item.h" // For SPItem cast +#include "document.h" // For SPDocument (corrected include) +#include "util/cast.h" // For cast function +#include "ui/widget/canvas.h" // For canvas access +#include "object/algorithms/removeoverlap.h" // For remove overlap preview +#include "2geom/rect.h" // For bounding box calculations namespace Inkscape::UI::Dialog { @@ -121,15 +130,44 @@ AlignAndDistribute::AlignAndDistribute(Inkscape::UI::Dialog::DialogBase *dlg) // clang-format on for (auto align_button : align_buttons) { - auto &button = get_widget(builder, align_button.first); - button.signal_clicked().connect( - sigc::bind(sigc::mem_fun(*this, &AlignAndDistribute::on_align_clicked), align_button.second)); + setup_hover_preview_for_button(align_button.first, align_button.second, + [this](const std::string& action) { preview_align(action); }); } - // ------------ Remove overlap ------------- + // ------------ Distribution buttons ------------- + std::vector> distribute_buttons = { + {"distribute-horizontal-left", "distribute-left" }, + {"distribute-horizontal-center", "distribute-hcenter" }, + {"distribute-horizontal-right", "distribute-right" }, + {"distribute-horizontal-gaps", "distribute-hgaps" }, + {"distribute-vertical-top", "distribute-top" }, + {"distribute-vertical-center", "distribute-vcenter" }, + {"distribute-vertical-bottom", "distribute-bottom" }, + {"distribute-vertical-gaps", "distribute-vgaps" } + }; + + for (auto distribute_button : distribute_buttons) { + setup_hover_preview_for_button(distribute_button.first, distribute_button.second, + [this](const std::string& action) { preview_distribute(action); }); + } + + // ------------ Rearrange buttons ------------- + std::vector> rearrange_buttons = { + {"rearrange-graph", "rearrange-graph" }, + {"exchange-positions", "exchange-positions" }, + {"exchange-positions-clockwise", "exchange-clockwise" }, + {"exchange-positions-random", "exchange-random" }, + {"unclump", "unclump" } + }; - remove_overlap_button.signal_clicked().connect( - sigc::mem_fun(*this, &AlignAndDistribute::on_remove_overlap_clicked)); + for (auto rearrange_button : rearrange_buttons) { + setup_hover_preview_for_button(rearrange_button.first, rearrange_button.second, + [this](const std::string& action) { preview_rearrange(action); }); + } + + // ------------ Remove overlap ------------- + setup_hover_preview_for_button("remove-overlap-button", "remove-overlap", + [this](const std::string& action) { preview_remove_overlap(); }); // ------------ Node Align ------------- @@ -158,13 +196,80 @@ AlignAndDistribute::AlignAndDistribute(Inkscape::UI::Dialog::DialogBase *dlg) auto set_icon_size_prefs = [prefs, this]() { int size = prefs->getIntLimited("/toolbox/tools/iconsize", -1, 16, 48); - Inkscape::UI::set_icon_sizes(this, size); + Inkscape::UI::set_icon_sizes(static_cast(this), size); }; // For now we are going to track the toolbox icon size, in the future we will have our own // dialog based icon sizes, perhaps done via css instead. _icon_sizes_changed = prefs->createObserver("/toolbox/tools/iconsize", set_icon_size_prefs); set_icon_size_prefs(); + + // Initialize hover preview preference if not set + if (prefs->getString("/dialogs/align/enable-hover-preview", "").empty()) { + prefs->setBool("/dialogs/align/enable-hover-preview", true); + } +} + +AlignAndDistribute::~AlignAndDistribute() +{ + // Clean up any active preview + if (_preview_active) { + end_preview(); + } + + // Disconnect timeout connection + if (_preview_timeout_connection.connected()) { + _preview_timeout_connection.disconnect(); + } +} + +void +AlignAndDistribute::setup_hover_preview_for_button(const char* button_id, const char* action_name, + std::function preview_func) +{ + auto &button = get_widget(builder, button_id); + + // Connect click handler + button.signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &AlignAndDistribute::on_button_clicked), std::string(action_name))); + + // Create motion controller for hover events + auto motion_controller = Gtk::EventControllerMotion::create(); + + // Store action string with controller + std::string action(action_name); + _motion_controllers[action] = motion_controller; + + // Connect hover handlers + motion_controller->signal_enter().connect([this, action, preview_func](double /*x*/, double /*y*/) { + // Check if preview is enabled + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool preview_enabled = prefs->getBool("/dialogs/align/enable-hover-preview", true); + if (!preview_enabled) return; + + // Cancel any existing timeout + if (_preview_timeout_connection.connected()) { + _preview_timeout_connection.disconnect(); + } + + // Store action and start timeout + _preview_action = action; + _preview_func = preview_func; + _preview_timeout_connection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &AlignAndDistribute::start_preview_timeout), 300); + }); + + motion_controller->signal_leave().connect([this]() { + // Cancel pending preview + if (_preview_timeout_connection.connected()) { + _preview_timeout_connection.disconnect(); + } + // End current preview + end_preview(); + }); + + // Add controller to button + button.add_controller(motion_controller); } void @@ -193,7 +298,6 @@ AlignAndDistribute::tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tool tool_changed(desktop); } - void AlignAndDistribute::on_align_as_group_clicked() { @@ -229,11 +333,63 @@ AlignAndDistribute::on_align_relative_node_changed() prefs->setString("/dialogs/align/nodes-align-to", align_relative_node.get_active_id()); } +void +AlignAndDistribute::on_button_clicked(std::string const &action) +{ + // If preview is active, we just confirm it (transforms already applied in preview) + if (_preview_active && _preview_action == action) { + confirm_preview(); + return; + } + + // Normal operation (no preview was active) - execute the actual command + execute_action(action); +} + +void +AlignAndDistribute::execute_action(const std::string& action) +{ + if (action.find("distribute") != std::string::npos) { + execute_distribute_action(action); + } else if (action == "remove-overlap") { + on_remove_overlap_clicked(); + } else if (action.find("rearrange") != std::string::npos || + action.find("exchange") != std::string::npos || + action == "unclump") { + execute_rearrange_action(action); + } else { + // Alignment action + on_align_clicked(action); + } +} + +void +AlignAndDistribute::execute_distribute_action(const std::string& action) +{ + auto app = Gio::Application::get_default(); + auto variant = Glib::Variant::create(action); + app->activate_action("object-distribute", variant); +} + +void +AlignAndDistribute::execute_rearrange_action(const std::string& action) +{ + auto app = Gio::Application::get_default(); + auto variant = Glib::Variant::create(action); + + if (action == "rearrange-graph") { + app->activate_action("object-rearrange-graph", variant); + } else if (action.find("exchange") != std::string::npos) { + app->activate_action("object-exchange-positions", variant); + } else if (action == "unclump") { + app->activate_action("object-unclump", variant); + } +} + void AlignAndDistribute::on_align_clicked(std::string const &align_to) { Glib::ustring argument = align_to; - argument += " " + align_relative_object.get_active_id(); if (align_move_as_group.get_active()) { @@ -280,6 +436,325 @@ AlignAndDistribute::on_align_node_clicked(std::string const &direction) } } +// ================== HOVER PREVIEW METHODS ================== + +bool +AlignAndDistribute::start_preview_timeout() +{ + if (_preview_func) { + start_preview(); + } + _preview_timeout_connection.disconnect(); + return false; +} + +void +AlignAndDistribute::start_preview() +{ + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + auto selection = desktop->getSelection(); + if (!selection || selection->isEmpty()) { + return; + } + + // If preview is already active, end it first + if (_preview_active) { + end_preview(); + } + + // Store original transforms + store_original_transforms(); + + // Set preview flag + _preview_active = true; + + // Execute preview function + if (_preview_func) { + _preview_func(_preview_action); + } + + // Update status + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, + "Preview active - click to confirm, move mouse away to cancel"); +} + +void +AlignAndDistribute::end_preview() +{ + if (!_preview_active) { + return; + } + + // Restore original transforms + restore_original_transforms(); + + _preview_active = false; + + // Clear status message + auto win = InkscapeApplication::instance()->get_active_window(); + if (win) { + if (auto desktop = win->get_desktop()) { + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, ""); + } + } +} + +void +AlignAndDistribute::confirm_preview() +{ + if (!_preview_active) { + return; + } + + _preview_active = false; + + // Clear preview data but don't restore (we want to keep the changes) + _original_transforms.clear(); + _preview_objects.clear(); + + // Clear status message + auto win = InkscapeApplication::instance()->get_active_window(); + if (win) { + if (auto desktop = win->get_desktop()) { + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, ""); + } + } +} + +void +AlignAndDistribute::store_original_transforms() +{ + _original_transforms.clear(); + _preview_objects.clear(); + + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + auto selection = desktop->getSelection(); + if (!selection) return; + + auto items = selection->items(); + for (auto item : items) { + _preview_objects.push_back(item); + _original_transforms.push_back(item->transform); + } +} + +void +AlignAndDistribute::restore_original_transforms() +{ + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + // Restore transforms + for (size_t i = 0; i < _preview_objects.size() && i < _original_transforms.size(); ++i) { + if (auto item = cast(_preview_objects[i])) { + item->set_transform(_original_transforms[i]); + } + } + + // Force canvas update + desktop->getCanvas()->redraw_all(); + + // Clear stored data + _original_transforms.clear(); + _preview_objects.clear(); +} + +// ================== PREVIEW IMPLEMENTATION METHODS ================== + +void +AlignAndDistribute::preview_align(const std::string& action) +{ + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + auto selection = desktop->getSelection(); + if (!selection || selection->isEmpty()) return; + + // Get the reference object/point for alignment + auto items = selection->items(); + if (items.empty()) return; + + // Calculate reference bounds based on align-to setting + Geom::OptRect reference_bounds; + std::string align_to = align_relative_object.get_active_id(); + + if (align_to == "page") { + auto doc = desktop->getDocument(); + double width = doc->getWidth().value("px"); + double height = doc->getHeight().value("px"); + reference_bounds = Geom::Rect(0, 0, width, height); + } else if (align_to == "selection") { + reference_bounds = selection->visualBounds(); + } else { + // For now, use selection bounds as fallback + reference_bounds = selection->visualBounds(); + } + + if (!reference_bounds) return; + + // Apply alignment preview PHONE each item + for (auto item : items) { + if (auto sp_item = cast(item)) { + auto item_bounds = sp_item->visualBounds(); + if (!item_bounds) continue; + + Geom::Affine transform = sp_item->transform; + Geom::Point offset(0, 0); + + // Calculate offset based on alignment type + if (action == "left") { + offset.x() = reference_bounds->left() - item_bounds->left(); + } else if (action == "hcenter") { + offset.x() = reference_bounds->midpoint().x() - item_bounds->midpoint().x(); + } else if (action == "right") { + offset.x() = reference_bounds->right() - item_bounds->right(); + } else if (action == "top") { + offset.y() = reference_bounds->top() - item_bounds->top(); + } else if (action == "vcenter") { + offset.y() = reference_bounds->midpoint().y() - item_bounds->midpoint().y(); + } else if (action == "bottom") { + offset.y() = reference_bounds->bottom() - item_bounds->bottom(); + } + + // Apply.Concurrent the offset + transform *= Geom::Translate(offset); + sp_item->set_transform(transform); + } + } + + // Force canvas update + desktop->getCanvas()->redraw_all(); +} + +void +AlignAndDistribute::preview_distribute(const std::string& action) +{ + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + auto selection = desktop->getSelection(); + if (!selection || selection->isEmpty()) return; + + // Convert items to vector and check size + std::vector item_vector; + auto items = selection->items(); + for (auto item : items) { + if (auto sp_item = cast(item)) { + item_vector.push_back(sp_item); + } + } + + if (item_vector.size() < 3) return; // Need at least 3 items to distribute + + // Sort items by position + std::vector sorted_items = item_vector; + + // Sort based on distribution type + if (action.find("horizontal") != std::string::npos || action.find("left") != std::string::npos || + action.find("right") != std::string::npos || action.find("hcenter") != std::string::npos) { + std::sort(sorted_items.begin(), sorted_items.end(), [](SPItem* a, SPItem* b) { + if (!a || !b) return false; + auto bounds_a = a->visualBounds(); + auto bounds_b = b->visualBounds(); + if (!bounds_a || !bounds_b) return false; + return bounds_a->midpoint().x() < bounds_b->midpoint().x(); + }); + } else { + std::sort(sorted_items.begin(), sorted_items.end(), [](SPItem* a, SPItem* b) { + if (!a || !b) return false; + auto bounds_a = a->visualBounds(); + auto bounds_b = b->visualBounds(); + if (!bounds_a || !bounds_b) return false; + return bounds_a->midpoint().y() < bounds_b->midpoint().y(); + }); + } + + // Calculate distribution spacing + auto first_bounds = sorted_items.front()->visualBounds(); + auto last_bounds = sorted_items.back()->visualBounds(); + if (!first_bounds || !last_bounds) return; + + double total_space; + if (action.find("horizontal") != std::string::npos) { + total_space = last_bounds->midpoint().x() - first_bounds->midpoint().x(); + } else { + total_space = last_bounds->midpoint().y() - first_bounds->midpoint().y(); + } + + double spacing = total_space / (sorted_items.size() - 1); + + // Apply distribution + for (size_t i = 1; i < sorted_items.size() - 1; ++i) { + auto item = sorted_items[i]; + auto item_bounds = item->visualBounds(); + if (!item_bounds) continue; + + Geom::Affine transform = item->transform; + Geom::Point target_pos; + + if (action.find("horizontal") != std::string::npos) { + target_pos.x() = first_bounds->midpoint().x() + spacing * i; + target_pos.y() = item_bounds->midpoint().y(); + } else { + target_pos.x() = item_bounds->midpoint().x(); + target_pos.y() = first_bounds->midpoint().y() + spacing * i; + } + + Geom::Point offset = target_pos - item_bounds->midpoint(); + transform *= Geom::Translate(offset); + item->set_transform(transform); + } + + desktop->getCanvas()->redraw_all(); +} + +void +AlignAndDistribute::preview_remove_overlap() +{ + // For simplicity, just show current positions (remove overlap is complex to preview) + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, + "Remove overlap preview - click to apply"); +} + +void +AlignAndDistribute::preview_rearrange(const std::string& action) +{ + // For simplicity, just show current positions (rearrange operations are complex to preview) + auto win = InkscapeApplication::instance()->get_active_window(); + if (!win) return; + + auto desktop = win->get_desktop(); + if (!desktop) return; + + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, + "Rearrange preview - click to apply"); +} + } // namespace Inkscape::UI::Dialog /* @@ -291,4 +766,4 @@ AlignAndDistribute::on_align_node_clicked(std::string const &direction) fill-column:99 End: */ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : \ No newline at end of file diff --git a/src/ui/dialog/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h index 3503cf5a3aab47f4e2f72d8697ff1887f3963aa0..9278ff978ba0c4184f045607d1415246d257d3b1 100644 --- a/src/ui/dialog/align-and-distribute.h +++ b/src/ui/dialog/align-and-distribute.h @@ -16,90 +16,128 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#ifndef INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H -#define INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H +#ifndef INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H +#define INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include "2geom/affine.h" #include "preferences.h" - -namespace Gtk { -class Builder; -class Button; -class ComboBox; -class Frame; -class SpinButton; -class ToggleButton; -} // namespace Gtk +#include "ui/tools/tool-base.h" class SPDesktop; +class SPObject; -namespace Inkscape { -namespace UI { - -namespace Tools { -class ToolBase; -} +namespace Inkscape::UI::Dialog { -namespace Dialog { class DialogBase; class AlignAndDistribute : public Gtk::Box { public: - AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg); - ~AlignAndDistribute() override = default; + AlignAndDistribute(Inkscape::UI::Dialog::DialogBase *dlg); + ~AlignAndDistribute(); void desktop_changed(SPDesktop* desktop); - void tool_changed(SPDesktop* desktop); // Need to show different widgets for node vs. other tools. - void tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* tool); private: + void tool_changed(SPDesktop* desktop); + void tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* tool); - // ********* Widgets ********** // + void on_align_as_group_clicked(); + void on_align_relative_object_changed(); + void on_align_relative_node_changed(); + void on_align_clicked(std::string const &align_to); + void on_remove_overlap_clicked(); + void on_align_node_clicked(std::string const &direction); + + // Button handling + void on_button_clicked(std::string const &action); + void execute_action(const std::string& action); + void execute_distribute_action(const std::string& action); + void execute_rearrange_action(const std::string& action); + + // Hover preview system + void setup_hover_preview_for_button(const char* button_id, const char* action_name, + std::function preview_func); + + // Preview methods + bool start_preview_timeout(); + void start_preview(); + void end_preview(); + void confirm_preview(); + void store_original_transforms(); + void restore_original_transforms(); + + // Preview implementations + void preview_align(const std::string& action); + void preview_distribute(const std::string& action); + void preview_remove_overlap(); + void preview_rearrange(const std::string& action); + + // UI Glib::RefPtr builder; - Gtk::Box &align_and_distribute_box; - Gtk::Box &align_and_distribute_object; // Hidden when node tool active. - Gtk::Frame &remove_overlap_frame; // Hidden when node tool active. - Gtk::Box &align_and_distribute_node; // Visible when node tool active. + Gtk::Box &align_and_distribute_object; + Gtk::Frame &remove_overlap_frame; + Gtk::Box &align_and_distribute_node; - // Align + // Object align + Gtk::ComboBox &align_relative_object; Gtk::ToggleButton &align_move_as_group; - Gtk::ComboBox &align_relative_object; - Gtk::ComboBox &align_relative_node; // Remove overlap - Gtk::Button &remove_overlap_button; - Gtk::SpinButton &remove_overlap_hgap; - Gtk::SpinButton &remove_overlap_vgap; - - // Valid relative alignment entries for single selection. - std::set single_selection_relative_categories = {"drawing", "page"}; - Glib::ustring single_selection_align_to = "page"; - Glib::ustring multi_selection_align_to; - bool single_item = false; - - // ********* Signal handlers ********** // - - void on_align_as_group_clicked(); - void on_align_relative_object_changed(); - void on_align_relative_node_changed(); + Gtk::Button &remove_overlap_button; + Gtk::SpinButton &remove_overlap_hgap; + Gtk::SpinButton &remove_overlap_vgap; - void on_align_clicked (std::string const &align_to); - void on_remove_overlap_clicked(); - void on_align_node_clicked (std::string const &align_to); + // Node + Gtk::ComboBox &align_relative_node; + // State sigc::connection tool_connection; - sigc::scoped_connection sel_changed; - Inkscape::PrefObserver _icon_sizes_changed; + sigc::connection sel_changed; + std::unique_ptr _icon_sizes_changed; + + bool single_item = false; + std::string single_selection_align_to = "first"; + std::string multi_selection_align_to = "selection"; + + std::set single_selection_relative_categories = { + "first", "last", "biggest", "smallest" + }; + + // Preview state + bool _preview_active = false; + std::string _preview_action; + std::function _preview_func; + sigc::connection _preview_timeout_connection; + + // Store original transforms for preview + std::vector _preview_objects; + std::vector _original_transforms; + + // Motion controllers for hover detection + std::map> _motion_controllers; }; -} // namespace Dialog -} // namespace UI -} // namespace Inkscape +} // namespace Inkscape::UI::Dialog -#endif // INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H +#endif // INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H /* Local Variables: @@ -110,4 +148,4 @@ private: fill-column:99 End: */ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : \ No newline at end of file