// SPDX-License-Identifier: GPL-2.0-or-later /* * A simple dialog for creating grid type arrangements of selected objects * * Authors: * Bob Jamison ( based off trace dialog) * John Cliff * Other dudes from The Inkscape Organization * Abhishek Sharma * Declara Denis * * Copyright (C) 2004 Bob Jamison * Copyright (C) 2004 John Cliff * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ //#define DEBUG_GRID_ARRANGE 1 #include "ui/dialog/grid-arrange-tab.h" #include #include #include #include #include <2geom/transforms.h> #include "preferences.h" #include "inkscape.h" #include "document.h" #include "document-undo.h" #include "desktop.h" #include "ui/icon-names.h" #include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog /** * Sort ObjectSet by an existing grid arrangement * * This is based on this paper here: DOI:10.1049/iet-ipr.2015.0126 * * @param items - The unsorted object set to sort. * @returns a new grid-sorted std::vector of SPItems. */ static std::vector grid_item_sort(Inkscape::ObjectSet *items) { std::vector results; Inkscape::ObjectSet rest; // 1. Find middle Y position of the largest top object. double box_top = items->visualBounds()->min()[Geom::Y]; double last_height = 0.0; double target = box_top; for (auto item : items->items()) { if (auto item_box = item->desktopVisualBounds()) { if (Geom::are_near(item_box->min()[Geom::Y], box_top, 2.0)) { if (item_box->height() > last_height) { last_height = item_box->height(); target = item_box->midpoint()[Geom::Y]; } } } } // 2. Loop through all remaining items for (auto item : items->items()) { // Items without visual bounds are completely ignored. if (auto item_box = item->desktopVisualBounds()) { auto radius = item_box->height() / 2; auto min = item_box->midpoint()[Geom::Y] - radius; auto max = item_box->midpoint()[Geom::Y] + radius; if (max > target && min < target) { // 2a. if the item's radius falls on the Y position above results.push_back(item); } else { // 2b. Save items not in this row for later rest.add(item); } } } // 3. Sort this single row according to the X position std::sort(results.begin(), results.end(), [](SPItem *a, SPItem *b) { // These boxes always exist because of the above filtering. return (a->desktopVisualBounds()->min()[Geom::X] < b->desktopVisualBounds()->min()[Geom::X]); }); if (results.size() == 0) { g_warning("Bad grid detection when sorting items!"); } else if (!rest.isEmpty()) { // 4. If there's any remaining, run this function again. auto sorted_rest = grid_item_sort(&rest); results.reserve(items->size()); results.insert(results.end(), sorted_rest.begin(), sorted_rest.end()); } return results; } namespace Inkscape { namespace UI { namespace Dialog { //######################################################################### //## E V E N T S //######################################################################### /* * * This arranges the selection in a grid pattern. * */ void GridArrangeTab::arrange() { // check for correct numbers in the row- and col-spinners on_col_spinbutton_changed(); on_row_spinbutton_changed(); // set padding to manual values double paddingx = XPadding.getValue("px"); double paddingy = YPadding.getValue("px"); int NoOfCols = NoOfColsSpinner.get_value_as_int(); int NoOfRows = NoOfRowsSpinner.get_value_as_int(); SPDesktop *desktop = Parent->getDesktop(); desktop->getDocument()->ensureUpToDate(); Inkscape::Selection *selection = desktop->getSelection(); if (!selection || selection->isEmpty()) return; auto sel_box = selection->documentBounds(SPItem::VISUAL_BBOX); if (sel_box.empty()) return; double grid_left = sel_box->min()[Geom::X]; double grid_top = sel_box->min()[Geom::Y]; // require the sorting done before we can calculate row heights etc. auto sorted = grid_item_sort(selection); // Calculate individual Row and Column sizes if necessary auto row_heights = std::vector(NoOfRows, 0.0); auto col_widths = std::vector(NoOfCols, 0.0); for(int i = 0; i < sorted.size(); i++) { if (Geom::OptRect box = sorted[i]->documentVisualBounds()) { double width = box->dimensions()[Geom::X]; double height = box->dimensions()[Geom::Y]; if (width > col_widths[(i % NoOfCols)]) { col_widths[(i % NoOfCols)] = width; } if (height > row_heights[(i / NoOfCols)]) { row_heights[(i / NoOfCols)] = height; } } } double col_width = *std::max_element(std::begin(col_widths), std::end(col_widths)); double row_height = *std::max_element(std::begin(row_heights), std::end(row_heights)); /// Make sure the top and left of the grid don't move by compensating for align values. if (RowHeightButton.get_active()){ grid_top = grid_top - (((row_height - row_heights[0]) / 2)*(VertAlign)); } if (ColumnWidthButton.get_active()){ grid_left = grid_left - (((col_width - col_widths[0]) /2)*(HorizAlign)); } // Calculate total widths and heights, allowing for columns and rows non uniformly sized. double last_col_padding = 0.0; double total_col_width = 0.0; if (ColumnWidthButton.get_active()){ total_col_width = col_width * NoOfCols; // Remember the amount of padding the box will lose on the right side last_col_padding = (col_width - col_widths[NoOfCols-1]) / 2; std::fill(col_widths.begin(), col_widths.end(), col_width); } else { total_col_width = std::accumulate(col_widths.begin(), col_widths.end(), 0); } double last_row_padding = 0.0; double total_row_height = 0.0; if (RowHeightButton.get_active()){ total_row_height = row_height * NoOfRows; // Remember the amount of padding the box will lose on the bottom side last_row_padding = (row_height - row_heights[NoOfRows-1]) / 2; std::fill(row_heights.begin(), row_heights.end(), row_height); } else { total_row_height = std::accumulate(row_heights.begin(), row_heights.end(), 0); } // Fit to bbox, calculate padding between rows accordingly. if (SpaceByBBoxRadioButton.get_active()) { paddingx = (sel_box->width() - total_col_width + last_col_padding) / (NoOfCols -1); paddingy = (sel_box->height() - total_row_height + last_row_padding) / (NoOfRows -1); } /* Horizontal align - Left = 0 Centre = 1 Right = 2 Vertical align - Top = 0 Middle = 1 Bottom = 2 X position is calculated by taking the grids left co-ord, adding the distance to the column, then adding 1/2 the spacing multiplied by the align variable above, Y position likewise, takes the top of the grid, adds the y to the current row then adds the padding in to align it. */ // Calculate row and column x and y coords required to allow for columns and rows which are non uniformly sized. std::vector col_xs = {0.0}; for (int col=1; col < NoOfCols; col++) { col_xs.push_back(col_widths[col-1] + paddingx + col_xs[col-1]); } std::vector row_ys = {0.0}; for (int row=1; row < NoOfRows; row++) { row_ys.push_back(row_heights[row-1] + paddingy + row_ys[row-1]); } int cnt = 0; std::vector::iterator it = sorted.begin(); for (int row_cnt=0; ((it != sorted.end()) && (row_cnt current_row; int col_cnt = 0; for(;it!=sorted.end()&&col_cntdocumentVisualBounds()) { width = vbox->dimensions()[Geom::X]; height = vbox->dimensions()[Geom::Y]; min = vbox->min(); } int row = cnt / NoOfCols; int col = cnt % NoOfCols; double new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col]; double new_y = grid_top + (((row_heights[row] - height)/2)*VertAlign) + row_ys[row]; Geom::Point move = Geom::Point(new_x, new_y) - min; Geom::Affine const affine = Geom::Affine(Geom::Translate(move)); item->set_i2d_affine(item->i2doc_affine() * affine * item->document->doc2dt()); item->doWriteTransform(item->transform); item->updateRepr(); cnt +=1; } } DocumentUndo::done(desktop->getDocument(), _("Arrange in a grid"), INKSCAPE_ICON("dialog-align-and-distribute")); } //######################################################################### //## E V E N T S //######################################################################### /** * changed value in # of columns spinbox. */ void GridArrangeTab::on_row_spinbutton_changed() { SPDesktop *desktop = Parent->getDesktop(); Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr; if (!selection) return; int selcount = (int) boost::distance(selection->items()); double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value()); NoOfRowsSpinner.set_value(NoOfRows); } /** * changed value in # of rows spinbox. */ void GridArrangeTab::on_col_spinbutton_changed() { SPDesktop *desktop = Parent->getDesktop(); Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr; if (!selection) return; int selcount = (int) boost::distance(selection->items()); double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value()); NoOfColsSpinner.set_value(NoOfCols); } /** * changed value in x padding spinbox. */ void GridArrangeTab::on_xpad_spinbutton_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setDouble("/dialogs/gridtiler/XPad", XPadding.getValue("px")); } /** * changed value in y padding spinbox. */ void GridArrangeTab::on_ypad_spinbutton_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setDouble("/dialogs/gridtiler/YPad", YPadding.getValue("px")); } /** * checked/unchecked autosize Rows button. */ void GridArrangeTab::on_RowSize_checkbutton_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (RowHeightButton.get_active()) { prefs->setDouble("/dialogs/gridtiler/AutoRowSize", 20); } else { prefs->setDouble("/dialogs/gridtiler/AutoRowSize", -20); } RowHeightBox.set_sensitive ( !RowHeightButton.get_active()); } /** * checked/unchecked autosize Rows button. */ void GridArrangeTab::on_ColSize_checkbutton_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (ColumnWidthButton.get_active()) { prefs->setDouble("/dialogs/gridtiler/AutoColSize", 20); } else { prefs->setDouble("/dialogs/gridtiler/AutoColSize", -20); } ColumnWidthBox.set_sensitive ( !ColumnWidthButton.get_active()); } /** * changed value in columns spinbox. */ void GridArrangeTab::on_rowSize_spinbutton_changed() { // quit if run by the attr_changed listener if (updating) { return; } // in turn, prevent listener from responding updating = true; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setDouble("/dialogs/gridtiler/RowHeight", RowHeightSpinner.get_value()); updating=false; } /** * changed value in rows spinbox. */ void GridArrangeTab::on_colSize_spinbutton_changed() { // quit if run by the attr_changed listener if (updating) { return; } // in turn, prevent listener from responding updating = true; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setDouble("/dialogs/gridtiler/ColWidth", ColumnWidthSpinner.get_value()); updating=false; } /** * changed Radio button in Spacing group. */ void GridArrangeTab::Spacing_button_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (SpaceManualRadioButton.get_active()) { prefs->setDouble("/dialogs/gridtiler/SpacingType", 20); } else { prefs->setDouble("/dialogs/gridtiler/SpacingType", -20); } XPadding.set_sensitive ( SpaceManualRadioButton.get_active()); YPadding.set_sensitive ( SpaceManualRadioButton.get_active()); } /** * changed Anchor selection widget. */ void GridArrangeTab::Align_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); VertAlign = AlignmentSelector.getVerticalAlignment(); prefs->setInt("/dialogs/gridtiler/VertAlign", VertAlign); HorizAlign = AlignmentSelector.getHorizontalAlignment(); prefs->setInt("/dialogs/gridtiler/HorizAlign", HorizAlign); } /** * Desktop selection changed */ void GridArrangeTab::updateSelection() { // quit if run by the attr_changed listener if (updating) { return; } // in turn, prevent listener from responding updating = true; SPDesktop *desktop = Parent->getDesktop(); Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr; std::vector items; if (selection) { items.insert(items.end(), selection->items().begin(), selection->items().end()); } if (!items.empty()) { int selcount = items.size(); if (NoOfColsSpinner.get_value() > 1 && NoOfRowsSpinner.get_value() > 1){ // Update the number of rows assuming number of columns wanted remains same. double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value()); NoOfRowsSpinner.set_value(NoOfRows); // if the selection has less than the number set for one row, reduce it appropriately if (selcount < NoOfColsSpinner.get_value()) { double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value()); NoOfColsSpinner.set_value(NoOfCols); } } else { double PerRow = ceil(sqrt(selcount)); double PerCol = ceil(sqrt(selcount)); NoOfRowsSpinner.set_value(PerRow); NoOfColsSpinner.set_value(PerCol); } } updating = false; } void GridArrangeTab::setDesktop(SPDesktop *desktop) { _selection_changed_connection.disconnect(); if (desktop) { updateSelection(); _selection_changed_connection = INKSCAPE.signal_selection_changed.connect( sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection))); } } //######################################################################### //## C O N S T R U C T O R / D E S T R U C T O R //######################################################################### /** * Constructor */ GridArrangeTab::GridArrangeTab(ArrangeDialog *parent) : Parent(parent), XPadding(_("X:"), _("Horizontal spacing between columns."), UNIT_TYPE_LINEAR, "", "object-columns", &PaddingUnitMenu), YPadding(_("Y:"), _("Vertical spacing between rows."), XPadding, "", "object-rows"), PaddingTable(Gtk::manage(new Gtk::Grid())) { // bool used by spin button callbacks to stop loops where they change each other. updating = false; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); auto _col1 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); auto _col2 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); auto _col3 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); Gtk::Box *contents = this; set_valign(Gtk::ALIGN_START); #define MARGIN 2 //##Set up the panel NoOfRowsLabel.set_text_with_mnemonic(_("_Rows:")); NoOfRowsLabel.set_mnemonic_widget(NoOfRowsSpinner); NoOfRowsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); NoOfRowsBox.pack_start(NoOfRowsLabel, false, false, MARGIN); NoOfRowsSpinner.set_digits(0); NoOfRowsSpinner.set_increments(1, 0); NoOfRowsSpinner.set_range(1.0, 10000.0); NoOfRowsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_col_spinbutton_changed)); NoOfRowsSpinner.set_tooltip_text(_("Number of rows")); NoOfRowsBox.pack_start(NoOfRowsSpinner, false, false, MARGIN); _col1->add_widget(NoOfRowsBox); RowHeightButton.set_label(_("Equal _height")); RowHeightButton.set_use_underline(true); double AutoRow = prefs->getDouble("/dialogs/gridtiler/AutoRowSize", 15); if (AutoRow>0) AutoRowSize=true; else AutoRowSize=false; RowHeightButton.set_active(AutoRowSize); NoOfRowsBox.pack_start(RowHeightButton, false, false, MARGIN); RowHeightButton.set_tooltip_text(_("If not set, each row has the height of the tallest object in it")); RowHeightButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_RowSize_checkbutton_changed)); SpinsHBox.pack_start(NoOfRowsBox, false, false, MARGIN); /*#### Label for X ####*/ padXByYLabel.set_label(" "); XByYLabelVBox.set_orientation(Gtk::ORIENTATION_VERTICAL); XByYLabelVBox.pack_start(padXByYLabel, false, false, MARGIN); XByYLabel.set_markup(" × "); XByYLabelVBox.pack_start(XByYLabel, false, false, MARGIN); SpinsHBox.pack_start(XByYLabelVBox, false, false, MARGIN); _col2->add_widget(XByYLabelVBox); /*#### Number of columns ####*/ NoOfColsLabel.set_text_with_mnemonic(_("_Columns:")); NoOfColsLabel.set_mnemonic_widget(NoOfColsSpinner); NoOfColsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); NoOfColsBox.pack_start(NoOfColsLabel, false, false, MARGIN); NoOfColsSpinner.set_digits(0); NoOfColsSpinner.set_increments(1, 0); NoOfColsSpinner.set_range(1.0, 10000.0); NoOfColsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_row_spinbutton_changed)); NoOfColsSpinner.set_tooltip_text(_("Number of columns")); NoOfColsBox.pack_start(NoOfColsSpinner, false, false, MARGIN); _col3->add_widget(NoOfColsBox); ColumnWidthButton.set_label(_("Equal _width")); ColumnWidthButton.set_use_underline(true); double AutoCol = prefs->getDouble("/dialogs/gridtiler/AutoColSize", 15); if (AutoCol>0) AutoColSize=true; else AutoColSize=false; ColumnWidthButton.set_active(AutoColSize); NoOfColsBox.pack_start(ColumnWidthButton, false, false, MARGIN); ColumnWidthButton.set_tooltip_text(_("If not set, each column has the width of the widest object in it")); ColumnWidthButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ColSize_checkbutton_changed)); SpinsHBox.pack_start(NoOfColsBox, false, false, MARGIN); TileBox.set_orientation(Gtk::ORIENTATION_VERTICAL); TileBox.pack_start(SpinsHBox, false, false, MARGIN); VertAlign = prefs->getInt("/dialogs/gridtiler/VertAlign", 1); HorizAlign = prefs->getInt("/dialogs/gridtiler/HorizAlign", 1); // Anchor selection widget AlignLabel.set_label(_("Alignment:")); AlignLabel.set_halign(Gtk::ALIGN_START); AlignLabel.set_valign(Gtk::ALIGN_CENTER); AlignmentSelector.setAlignment(HorizAlign, VertAlign); AlignmentSelector.on_selectionChanged().connect(sigc::mem_fun(*this, &GridArrangeTab::Align_changed)); TileBox.pack_start(AlignLabel, false, false, MARGIN); TileBox.pack_start(AlignmentSelector, true, false, MARGIN); { /*#### Radio buttons to control spacing manually or to fit selection bbox ####*/ SpaceByBBoxRadioButton.set_label(_("_Fit into selection box")); SpaceByBBoxRadioButton.set_use_underline (true); SpaceByBBoxRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); SpacingGroup = SpaceByBBoxRadioButton.get_group(); SpacingVBox.pack_start(SpaceByBBoxRadioButton, false, false, MARGIN); SpaceManualRadioButton.set_label(_("_Set spacing:")); SpaceManualRadioButton.set_use_underline (true); SpaceManualRadioButton.set_group(SpacingGroup); SpaceManualRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); SpacingVBox.pack_start(SpaceManualRadioButton, false, false, MARGIN); TileBox.pack_start(SpacingVBox, false, false, MARGIN); } { /*#### Padding ####*/ PaddingUnitMenu.setUnitType(UNIT_TYPE_LINEAR); PaddingUnitMenu.setUnit("px"); YPadding.setDigits(5); YPadding.setIncrements(0.2, 0); YPadding.setRange(-10000, 10000); double yPad = prefs->getDouble("/dialogs/gridtiler/YPad", 15); YPadding.setValue(yPad, "px"); YPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ypad_spinbutton_changed)); XPadding.setDigits(5); XPadding.setIncrements(0.2, 0); XPadding.setRange(-10000, 10000); double xPad = prefs->getDouble("/dialogs/gridtiler/XPad", 15); XPadding.setValue(xPad, "px"); XPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_xpad_spinbutton_changed)); } PaddingTable->set_border_width(MARGIN); PaddingTable->set_row_spacing(MARGIN); PaddingTable->set_column_spacing(MARGIN); PaddingTable->attach(XPadding, 0, 0, 1, 1); PaddingTable->attach(PaddingUnitMenu, 1, 0, 1, 1); PaddingTable->attach(YPadding, 0, 1, 1, 1); TileBox.pack_start(*PaddingTable, false, false, MARGIN); contents->set_border_width(4); contents->pack_start(TileBox); double SpacingType = prefs->getDouble("/dialogs/gridtiler/SpacingType", 15); if (SpacingType>0) { ManualSpacing=true; } else { ManualSpacing=false; } SpaceManualRadioButton.set_active(ManualSpacing); SpaceByBBoxRadioButton.set_active(!ManualSpacing); XPadding.set_sensitive (ManualSpacing); YPadding.set_sensitive (ManualSpacing); show_all_children(); } GridArrangeTab::~GridArrangeTab() { setDesktop(nullptr); } } //namespace Dialog } //namespace UI } //namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :