From 35a96bde514a8897f6f0fcc41c5833bf63df2e2a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 18:29:01 +0200 Subject: Adding upstream version 1.0.2. Signed-off-by: Daniel Baumann --- src/ui/CMakeLists.txt | 477 +++ src/ui/README | 21 + src/ui/cache/README | 3 + src/ui/cache/svg_preview_cache.cpp | 142 + src/ui/cache/svg_preview_cache.h | 65 + src/ui/clipboard.cpp | 1682 +++++++++++ src/ui/clipboard.h | 75 + src/ui/contextmenu.cpp | 1008 +++++++ src/ui/contextmenu.h | 225 ++ src/ui/control-manager.cpp | 509 ++++ src/ui/control-manager.h | 96 + src/ui/control-types.h | 58 + src/ui/desktop/README | 27 + src/ui/desktop/menubar.cpp | 635 ++++ src/ui/desktop/menubar.h | 48 + src/ui/dialog-events.cpp | 244 ++ src/ui/dialog-events.h | 76 + src/ui/dialog/aboutbox.cpp | 224 ++ src/ui/dialog/aboutbox.h | 66 + src/ui/dialog/align-and-distribute.cpp | 1339 +++++++++ src/ui/dialog/align-and-distribute.h | 223 ++ src/ui/dialog/arrange-tab.h | 55 + src/ui/dialog/attrdialog.cpp | 682 +++++ src/ui/dialog/attrdialog.h | 128 + src/ui/dialog/behavior.h | 104 + src/ui/dialog/calligraphic-profile-rename.cpp | 142 + src/ui/dialog/calligraphic-profile-rename.h | 87 + src/ui/dialog/clonetiler.cpp | 2834 ++++++++++++++++++ src/ui/dialog/clonetiler.h | 226 ++ src/ui/dialog/color-item.cpp | 751 +++++ src/ui/dialog/color-item.h | 130 + src/ui/dialog/debug.cpp | 259 ++ src/ui/dialog/debug.h | 101 + src/ui/dialog/desktop-tracker.cpp | 154 + src/ui/dialog/desktop-tracker.h | 69 + src/ui/dialog/dialog-manager.cpp | 310 ++ src/ui/dialog/dialog-manager.h | 74 + src/ui/dialog/dialog.cpp | 370 +++ src/ui/dialog/dialog.h | 179 ++ src/ui/dialog/dock-behavior.cpp | 297 ++ src/ui/dialog/dock-behavior.h | 104 + src/ui/dialog/document-metadata.cpp | 223 ++ src/ui/dialog/document-metadata.h | 86 + src/ui/dialog/document-properties.cpp | 1708 +++++++++++ src/ui/dialog/document-properties.h | 262 ++ src/ui/dialog/export.cpp | 1948 ++++++++++++ src/ui/dialog/export.h | 367 +++ src/ui/dialog/extension-editor.cpp | 221 ++ src/ui/dialog/extension-editor.h | 93 + src/ui/dialog/extensions.cpp | 120 + src/ui/dialog/extensions.h | 57 + src/ui/dialog/filedialog.cpp | 201 ++ src/ui/dialog/filedialog.h | 254 ++ src/ui/dialog/filedialogimpl-gtkmm.cpp | 867 ++++++ src/ui/dialog/filedialogimpl-gtkmm.h | 313 ++ src/ui/dialog/filedialogimpl-win32.cpp | 1937 ++++++++++++ src/ui/dialog/filedialogimpl-win32.h | 393 +++ src/ui/dialog/fill-and-stroke.cpp | 207 ++ src/ui/dialog/fill-and-stroke.h | 100 + src/ui/dialog/filter-editor.cpp | 120 + src/ui/dialog/filter-editor.h | 53 + src/ui/dialog/filter-effects-dialog.cpp | 3114 ++++++++++++++++++++ src/ui/dialog/filter-effects-dialog.h | 346 +++ src/ui/dialog/find.cpp | 1076 +++++++ src/ui/dialog/find.h | 320 ++ src/ui/dialog/floating-behavior.cpp | 168 ++ src/ui/dialog/floating-behavior.h | 92 + src/ui/dialog/font-substitution.cpp | 271 ++ src/ui/dialog/font-substitution.h | 58 + src/ui/dialog/glyphs.cpp | 822 ++++++ src/ui/dialog/glyphs.h | 97 + src/ui/dialog/grid-arrange-tab.cpp | 776 +++++ src/ui/dialog/grid-arrange-tab.h | 148 + src/ui/dialog/guides.cpp | 358 +++ src/ui/dialog/guides.h | 103 + src/ui/dialog/icon-preview.cpp | 682 +++++ src/ui/dialog/icon-preview.h | 119 + src/ui/dialog/inkscape-preferences.cpp | 2772 +++++++++++++++++ src/ui/dialog/inkscape-preferences.h | 620 ++++ src/ui/dialog/input.cpp | 1792 +++++++++++ src/ui/dialog/input.h | 47 + src/ui/dialog/knot-properties.cpp | 207 ++ src/ui/dialog/knot-properties.h | 97 + src/ui/dialog/layer-properties.cpp | 424 +++ src/ui/dialog/layer-properties.h | 177 ++ src/ui/dialog/layers.cpp | 1004 +++++++ src/ui/dialog/layers.h | 152 + src/ui/dialog/livepatheffect-add.cpp | 933 ++++++ src/ui/dialog/livepatheffect-add.h | 147 + src/ui/dialog/livepatheffect-editor.cpp | 634 ++++ src/ui/dialog/livepatheffect-editor.h | 152 + src/ui/dialog/lpe-fillet-chamfer-properties.cpp | 276 ++ src/ui/dialog/lpe-fillet-chamfer-properties.h | 114 + src/ui/dialog/lpe-powerstroke-properties.cpp | 199 ++ src/ui/dialog/lpe-powerstroke-properties.h | 92 + src/ui/dialog/memory.cpp | 247 ++ src/ui/dialog/memory.h | 54 + src/ui/dialog/messages.cpp | 216 ++ src/ui/dialog/messages.h | 103 + src/ui/dialog/new-from-template.cpp | 72 + src/ui/dialog/new-from-template.h | 45 + src/ui/dialog/object-attributes.cpp | 218 ++ src/ui/dialog/object-attributes.h | 123 + src/ui/dialog/object-properties.cpp | 605 ++++ src/ui/dialog/object-properties.h | 148 + src/ui/dialog/objects.cpp | 2363 +++++++++++++++ src/ui/dialog/objects.h | 279 ++ src/ui/dialog/paint-servers.cpp | 505 ++++ src/ui/dialog/paint-servers.h | 88 + src/ui/dialog/panel-dialog.h | 208 ++ src/ui/dialog/polar-arrange-tab.cpp | 408 +++ src/ui/dialog/polar-arrange-tab.h | 104 + src/ui/dialog/print-colors-preview-dialog.cpp | 103 + src/ui/dialog/print-colors-preview-dialog.h | 48 + src/ui/dialog/print.cpp | 263 ++ src/ui/dialog/print.h | 80 + src/ui/dialog/save-template-dialog.cpp | 101 + src/ui/dialog/save-template-dialog.h | 60 + src/ui/dialog/selectorsdialog.cpp | 1508 ++++++++++ src/ui/dialog/selectorsdialog.h | 209 ++ src/ui/dialog/spellcheck.cpp | 815 +++++ src/ui/dialog/spellcheck.h | 301 ++ src/ui/dialog/styledialog.cpp | 1690 +++++++++++ src/ui/dialog/styledialog.h | 208 ++ src/ui/dialog/svg-fonts-dialog.cpp | 1067 +++++++ src/ui/dialog/svg-fonts-dialog.h | 283 ++ src/ui/dialog/svg-preview.cpp | 476 +++ src/ui/dialog/svg-preview.h | 123 + src/ui/dialog/swatches.cpp | 1418 +++++++++ src/ui/dialog/swatches.h | 110 + src/ui/dialog/symbols.cpp | 1403 +++++++++ src/ui/dialog/symbols.h | 183 ++ src/ui/dialog/tags.cpp | 1125 +++++++ src/ui/dialog/tags.h | 174 ++ src/ui/dialog/template-load-tab.cpp | 337 +++ src/ui/dialog/template-load-tab.h | 119 + src/ui/dialog/template-widget.cpp | 152 + src/ui/dialog/template-widget.h | 51 + src/ui/dialog/text-edit.cpp | 585 ++++ src/ui/dialog/text-edit.h | 217 ++ src/ui/dialog/tile.cpp | 81 + src/ui/dialog/tile.h | 80 + src/ui/dialog/tracedialog.cpp | 380 +++ src/ui/dialog/tracedialog.h | 69 + src/ui/dialog/transformation.cpp | 1171 ++++++++ src/ui/dialog/transformation.h | 258 ++ src/ui/dialog/undo-history.cpp | 406 +++ src/ui/dialog/undo-history.h | 179 ++ src/ui/dialog/xml-tree.cpp | 968 ++++++ src/ui/dialog/xml-tree.h | 265 ++ src/ui/drag-and-drop.cpp | 534 ++++ src/ui/drag-and-drop.h | 51 + src/ui/draw-anchor.cpp | 108 + src/ui/draw-anchor.h | 61 + src/ui/event-debug.h | 122 + src/ui/icon-loader.cpp | 137 + src/ui/icon-loader.h | 29 + src/ui/icon-names.h | 32 + src/ui/interface.cpp | 268 ++ src/ui/interface.h | 76 + src/ui/monitor.cpp | 68 + src/ui/monitor.h | 38 + src/ui/pixmaps/README | 2 + src/ui/pixmaps/cursor-3dbox.xpm | 38 + src/ui/pixmaps/cursor-adj-a.xpm | 38 + src/ui/pixmaps/cursor-adj-h.xpm | 38 + src/ui/pixmaps/cursor-adj-l.xpm | 38 + src/ui/pixmaps/cursor-adj-s.xpm | 38 + src/ui/pixmaps/cursor-calligraphy.xpm | 38 + src/ui/pixmaps/cursor-connector.xpm | 38 + src/ui/pixmaps/cursor-crosshairs.xpm | 38 + src/ui/pixmaps/cursor-dropper-f.xpm | 39 + src/ui/pixmaps/cursor-dropper-s.xpm | 39 + src/ui/pixmaps/cursor-dropping-f.xpm | 39 + src/ui/pixmaps/cursor-dropping-s.xpm | 39 + src/ui/pixmaps/cursor-ellipse.xpm | 40 + src/ui/pixmaps/cursor-eraser.xpm | 38 + src/ui/pixmaps/cursor-gradient-add.xpm | 38 + src/ui/pixmaps/cursor-gradient.xpm | 38 + src/ui/pixmaps/cursor-measure.xpm | 38 + src/ui/pixmaps/cursor-node-d.xpm | 38 + src/ui/pixmaps/cursor-node.xpm | 38 + src/ui/pixmaps/cursor-paintbucket.xpm | 38 + src/ui/pixmaps/cursor-pen.xpm | 38 + src/ui/pixmaps/cursor-pencil.xpm | 38 + src/ui/pixmaps/cursor-rect.xpm | 40 + src/ui/pixmaps/cursor-select-d.xpm | 38 + src/ui/pixmaps/cursor-select-m.xpm | 38 + src/ui/pixmaps/cursor-select.xpm | 38 + src/ui/pixmaps/cursor-spiral.xpm | 38 + src/ui/pixmaps/cursor-spray-move.xpm | 38 + src/ui/pixmaps/cursor-spray.xpm | 38 + src/ui/pixmaps/cursor-star.xpm | 40 + src/ui/pixmaps/cursor-text-insert.xpm | 38 + src/ui/pixmaps/cursor-text.xpm | 38 + src/ui/pixmaps/cursor-tweak-attract.xpm | 38 + src/ui/pixmaps/cursor-tweak-color.xpm | 38 + src/ui/pixmaps/cursor-tweak-less.xpm | 38 + src/ui/pixmaps/cursor-tweak-more.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-in.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-jitter.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-out.xpm | 38 + src/ui/pixmaps/cursor-tweak-move.xpm | 38 + src/ui/pixmaps/cursor-tweak-push.xpm | 38 + src/ui/pixmaps/cursor-tweak-repel.xpm | 38 + src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm | 38 + .../cursor-tweak-rotate-counterclockwise.xpm | 38 + src/ui/pixmaps/cursor-tweak-roughen.xpm | 38 + src/ui/pixmaps/cursor-tweak-scale-down.xpm | 38 + src/ui/pixmaps/cursor-tweak-scale-up.xpm | 38 + src/ui/pixmaps/cursor-tweak-thicken.xpm | 38 + src/ui/pixmaps/cursor-tweak-thin.xpm | 38 + src/ui/pixmaps/cursor-zoom-out.xpm | 38 + src/ui/pixmaps/cursor-zoom.xpm | 38 + src/ui/pixmaps/handles.xpm | 159 + src/ui/pref-pusher.cpp | 70 + src/ui/pref-pusher.h | 80 + src/ui/previewable.h | 64 + src/ui/previewholder.cpp | 447 +++ src/ui/previewholder.h | 90 + src/ui/selected-color.cpp | 159 + src/ui/selected-color.h | 99 + src/ui/shape-editor-knotholders.cpp | 1980 +++++++++++++ src/ui/shape-editor.cpp | 218 ++ src/ui/shape-editor.h | 71 + src/ui/simple-pref-pusher.cpp | 49 + src/ui/simple-pref-pusher.h | 65 + src/ui/tool-factory.cpp | 101 + src/ui/tool-factory.h | 43 + src/ui/tool/commit-events.h | 52 + src/ui/tool/control-point-selection.cpp | 773 +++++ src/ui/tool/control-point-selection.h | 178 ++ src/ui/tool/control-point.cpp | 637 ++++ src/ui/tool/control-point.h | 411 +++ src/ui/tool/curve-drag-point.cpp | 231 ++ src/ui/tool/curve-drag-point.h | 77 + src/ui/tool/event-utils.cpp | 162 + src/ui/tool/event-utils.h | 133 + src/ui/tool/manipulator.cpp | 90 + src/ui/tool/manipulator.h | 174 ++ src/ui/tool/modifier-tracker.cpp | 94 + src/ui/tool/modifier-tracker.h | 55 + src/ui/tool/multi-path-manipulator.cpp | 888 ++++++ src/ui/tool/multi-path-manipulator.h | 154 + src/ui/tool/node-types.h | 49 + src/ui/tool/node.cpp | 1924 ++++++++++++ src/ui/tool/node.h | 527 ++++ src/ui/tool/path-manipulator.cpp | 1756 +++++++++++ src/ui/tool/path-manipulator.h | 180 ++ src/ui/tool/selectable-control-point.cpp | 147 + src/ui/tool/selectable-control-point.h | 78 + src/ui/tool/selector.cpp | 150 + src/ui/tool/selector.h | 60 + src/ui/tool/shape-record.h | 62 + src/ui/tool/transform-handle-set.cpp | 867 ++++++ src/ui/tool/transform-handle-set.h | 142 + src/ui/toolbar/arc-toolbar.cpp | 561 ++++ src/ui/toolbar/arc-toolbar.h | 116 + src/ui/toolbar/box3d-toolbar.cpp | 418 +++ src/ui/toolbar/box3d-toolbar.h | 106 + src/ui/toolbar/calligraphy-toolbar.cpp | 599 ++++ src/ui/toolbar/calligraphy-toolbar.h | 103 + src/ui/toolbar/connector-toolbar.cpp | 437 +++ src/ui/toolbar/connector-toolbar.h | 93 + src/ui/toolbar/dropper-toolbar.cpp | 116 + src/ui/toolbar/dropper-toolbar.h | 70 + src/ui/toolbar/eraser-toolbar.cpp | 335 +++ src/ui/toolbar/eraser-toolbar.h | 95 + src/ui/toolbar/gradient-toolbar.cpp | 1178 ++++++++ src/ui/toolbar/gradient-toolbar.h | 102 + src/ui/toolbar/lpe-toolbar.cpp | 418 +++ src/ui/toolbar/lpe-toolbar.h | 101 + src/ui/toolbar/measure-toolbar.cpp | 452 +++ src/ui/toolbar/measure-toolbar.h | 91 + src/ui/toolbar/mesh-toolbar.cpp | 621 ++++ src/ui/toolbar/mesh-toolbar.h | 97 + src/ui/toolbar/node-toolbar.cpp | 651 ++++ src/ui/toolbar/node-toolbar.h | 115 + src/ui/toolbar/paintbucket-toolbar.cpp | 221 ++ src/ui/toolbar/paintbucket-toolbar.h | 72 + src/ui/toolbar/pencil-toolbar.cpp | 622 ++++ src/ui/toolbar/pencil-toolbar.h | 99 + src/ui/toolbar/rect-toolbar.cpp | 407 +++ src/ui/toolbar/rect-toolbar.h | 113 + src/ui/toolbar/select-toolbar.cpp | 508 ++++ src/ui/toolbar/select-toolbar.h | 82 + src/ui/toolbar/snap-toolbar.cpp | 402 +++ src/ui/toolbar/snap-toolbar.h | 70 + src/ui/toolbar/spiral-toolbar.cpp | 304 ++ src/ui/toolbar/spiral-toolbar.h | 98 + src/ui/toolbar/spray-toolbar.cpp | 550 ++++ src/ui/toolbar/spray-toolbar.h | 107 + src/ui/toolbar/star-toolbar.cpp | 564 ++++ src/ui/toolbar/star-toolbar.h | 108 + src/ui/toolbar/text-toolbar.cpp | 2540 ++++++++++++++++ src/ui/toolbar/text-toolbar.h | 149 + src/ui/toolbar/toolbar.cpp | 102 + src/ui/toolbar/toolbar.h | 67 + src/ui/toolbar/tweak-toolbar.cpp | 347 +++ src/ui/toolbar/tweak-toolbar.h | 89 + src/ui/toolbar/zoom-toolbar.cpp | 85 + src/ui/toolbar/zoom-toolbar.h | 62 + src/ui/tools-switch.cpp | 195 ++ src/ui/tools-switch.h | 65 + src/ui/tools/arc-tool.cpp | 488 +++ src/ui/tools/arc-tool.h | 83 + src/ui/tools/box3d-tool.cpp | 614 ++++ src/ui/tools/box3d-tool.h | 109 + src/ui/tools/calligraphic-tool.cpp | 1203 ++++++++ src/ui/tools/calligraphic-tool.h | 103 + src/ui/tools/connector-tool.cpp | 1393 +++++++++ src/ui/tools/connector-tool.h | 168 ++ src/ui/tools/dropper-tool.cpp | 414 +++ src/ui/tools/dropper-tool.h | 87 + src/ui/tools/dynamic-base.cpp | 166 ++ src/ui/tools/dynamic-base.h | 130 + src/ui/tools/eraser-tool.cpp | 1119 +++++++ src/ui/tools/eraser-tool.h | 84 + src/ui/tools/flood-tool.cpp | 1254 ++++++++ src/ui/tools/flood-tool.h | 72 + src/ui/tools/freehand-base.cpp | 1112 +++++++ src/ui/tools/freehand-base.h | 164 ++ src/ui/tools/gradient-tool.cpp | 932 ++++++ src/ui/tools/gradient-tool.h | 77 + src/ui/tools/lpe-tool.cpp | 494 ++++ src/ui/tools/lpe-tool.h | 100 + src/ui/tools/measure-tool.cpp | 1466 +++++++++ src/ui/tools/measure-tool.h | 110 + src/ui/tools/mesh-tool.cpp | 1098 +++++++ src/ui/tools/mesh-tool.h | 87 + src/ui/tools/node-tool.cpp | 848 ++++++ src/ui/tools/node-tool.h | 118 + src/ui/tools/pen-tool.cpp | 2104 +++++++++++++ src/ui/tools/pen-tool.h | 166 ++ src/ui/tools/pencil-tool.cpp | 1239 ++++++++ src/ui/tools/pencil-tool.h | 104 + src/ui/tools/rect-tool.cpp | 503 ++++ src/ui/tools/rect-tool.h | 64 + src/ui/tools/select-tool.cpp | 1171 ++++++++ src/ui/tools/select-tool.h | 72 + src/ui/tools/spiral-tool.cpp | 444 +++ src/ui/tools/spiral-tool.h | 65 + src/ui/tools/spray-tool.cpp | 1533 ++++++++++ src/ui/tools/spray-tool.h | 151 + src/ui/tools/star-tool.cpp | 461 +++ src/ui/tools/star-tool.h | 75 + src/ui/tools/text-tool.cpp | 1897 ++++++++++++ src/ui/tools/text-tool.h | 108 + src/ui/tools/tool-base.cpp | 1630 ++++++++++ src/ui/tools/tool-base.h | 284 ++ src/ui/tools/tweak-tool.cpp | 1535 ++++++++++ src/ui/tools/tweak-tool.h | 107 + src/ui/tools/zoom-tool.cpp | 243 ++ src/ui/tools/zoom-tool.h | 48 + src/ui/util.cpp | 38 + src/ui/util.h | 31 + src/ui/uxmanager.cpp | 246 ++ src/ui/uxmanager.h | 61 + src/ui/view/README | 51 + src/ui/view/edit-widget-interface.h | 178 ++ src/ui/view/svg-view-widget.cpp | 266 ++ src/ui/view/svg-view-widget.h | 89 + src/ui/view/view-widget.cpp | 102 + src/ui/view/view-widget.h | 107 + src/ui/view/view.cpp | 138 + src/ui/view/view.h | 148 + src/ui/widget/alignment-selector.cpp | 78 + src/ui/widget/alignment-selector.h | 53 + src/ui/widget/anchor-selector.cpp | 97 + src/ui/widget/anchor-selector.h | 62 + src/ui/widget/attr-widget.h | 185 ++ src/ui/widget/button.cpp | 273 ++ src/ui/widget/button.h | 89 + src/ui/widget/clipmaskicon.cpp | 123 + src/ui/widget/clipmaskicon.h | 86 + src/ui/widget/color-entry.cpp | 157 + src/ui/widget/color-entry.h | 58 + src/ui/widget/color-icc-selector.cpp | 1074 +++++++ src/ui/widget/color-icc-selector.h | 75 + src/ui/widget/color-notebook.cpp | 341 +++ src/ui/widget/color-notebook.h | 89 + src/ui/widget/color-picker.cpp | 144 + src/ui/widget/color-picker.h | 114 + src/ui/widget/color-preview.cpp | 171 ++ src/ui/widget/color-preview.h | 57 + src/ui/widget/color-scales.cpp | 736 +++++ src/ui/widget/color-scales.h | 110 + src/ui/widget/color-slider.cpp | 536 ++++ src/ui/widget/color-slider.h | 93 + src/ui/widget/color-wheel-selector.cpp | 237 ++ src/ui/widget/color-wheel-selector.h | 83 + src/ui/widget/combo-box-entry-tool-item.cpp | 691 +++++ src/ui/widget/combo-box-entry-tool-item.h | 157 + src/ui/widget/combo-enums.h | 224 ++ src/ui/widget/combo-tool-item.cpp | 290 ++ src/ui/widget/combo-tool-item.h | 136 + src/ui/widget/dash-selector.cpp | 309 ++ src/ui/widget/dash-selector.h | 112 + src/ui/widget/dock-item.cpp | 528 ++++ src/ui/widget/dock-item.h | 159 + src/ui/widget/dock.cpp | 305 ++ src/ui/widget/dock.h | 108 + src/ui/widget/entity-entry.cpp | 207 ++ src/ui/widget/entity-entry.h | 85 + src/ui/widget/entry.cpp | 30 + src/ui/widget/entry.h | 45 + src/ui/widget/filter-effect-chooser.cpp | 194 ++ src/ui/widget/filter-effect-chooser.h | 95 + src/ui/widget/font-button.cpp | 58 + src/ui/widget/font-button.h | 63 + src/ui/widget/font-selector-toolbar.cpp | 301 ++ src/ui/widget/font-selector-toolbar.h | 120 + src/ui/widget/font-selector.cpp | 450 +++ src/ui/widget/font-selector.h | 163 + src/ui/widget/font-variants.cpp | 1461 +++++++++ src/ui/widget/font-variants.h | 222 ++ src/ui/widget/font-variations.cpp | 179 ++ src/ui/widget/font-variations.h | 126 + src/ui/widget/frame.cpp | 80 + src/ui/widget/frame.h | 75 + src/ui/widget/highlight-picker.cpp | 169 ++ src/ui/widget/highlight-picker.h | 73 + src/ui/widget/iconrenderer.cpp | 121 + src/ui/widget/iconrenderer.h | 84 + src/ui/widget/imagetoggler.cpp | 113 + src/ui/widget/imagetoggler.h | 89 + src/ui/widget/ink-color-wheel.cpp | 726 +++++ src/ui/widget/ink-color-wheel.h | 86 + src/ui/widget/ink-flow-box.cpp | 143 + src/ui/widget/ink-flow-box.h | 68 + src/ui/widget/ink-ruler.cpp | 473 +++ src/ui/widget/ink-ruler.h | 80 + src/ui/widget/ink-spinscale.cpp | 285 ++ src/ui/widget/ink-spinscale.h | 96 + src/ui/widget/insertordericon.cpp | 118 + src/ui/widget/insertordericon.h | 85 + src/ui/widget/label-tool-item.cpp | 66 + src/ui/widget/label-tool-item.h | 51 + src/ui/widget/labelled.cpp | 110 + src/ui/widget/labelled.h | 89 + src/ui/widget/layer-selector.cpp | 616 ++++ src/ui/widget/layer-selector.h | 114 + src/ui/widget/layertypeicon.cpp | 113 + src/ui/widget/layertypeicon.h | 91 + src/ui/widget/licensor.cpp | 156 + src/ui/widget/licensor.h | 57 + src/ui/widget/notebook-page.cpp | 47 + src/ui/widget/notebook-page.h | 59 + src/ui/widget/object-composite-settings.cpp | 325 ++ src/ui/widget/object-composite-settings.h | 81 + src/ui/widget/page-sizer.cpp | 781 +++++ src/ui/widget/page-sizer.h | 307 ++ src/ui/widget/pages-skeleton.h | 154 + src/ui/widget/panel.cpp | 176 ++ src/ui/widget/panel.h | 139 + src/ui/widget/point.cpp | 180 ++ src/ui/widget/point.h | 196 ++ src/ui/widget/preferences-widget.cpp | 1023 +++++++ src/ui/widget/preferences-widget.h | 315 ++ src/ui/widget/preview.cpp | 502 ++++ src/ui/widget/preview.h | 163 + src/ui/widget/random.cpp | 101 + src/ui/widget/random.h | 125 + src/ui/widget/registered-enums.h | 99 + src/ui/widget/registered-widget.cpp | 845 ++++++ src/ui/widget/registered-widget.h | 458 +++ src/ui/widget/registry.cpp | 53 + src/ui/widget/registry.h | 48 + src/ui/widget/rendering-options.cpp | 122 + src/ui/widget/rendering-options.h | 68 + src/ui/widget/rotateable.cpp | 180 ++ src/ui/widget/rotateable.h | 71 + src/ui/widget/scalar-unit.cpp | 263 ++ src/ui/widget/scalar-unit.h | 191 ++ src/ui/widget/scalar.cpp | 173 ++ src/ui/widget/scalar.h | 188 ++ src/ui/widget/selected-style.cpp | 1488 ++++++++++ src/ui/widget/selected-style.h | 296 ++ src/ui/widget/spin-button-tool-item.cpp | 532 ++++ src/ui/widget/spin-button-tool-item.h | 95 + src/ui/widget/spin-scale.cpp | 226 ++ src/ui/widget/spin-scale.h | 110 + src/ui/widget/spin-slider.cpp | 223 ++ src/ui/widget/spin-slider.h | 106 + src/ui/widget/spinbutton.cpp | 137 + src/ui/widget/spinbutton.h | 117 + src/ui/widget/style-subject.cpp | 189 ++ src/ui/widget/style-subject.h | 133 + src/ui/widget/style-swatch.cpp | 373 +++ src/ui/widget/style-swatch.h | 105 + src/ui/widget/text.cpp | 60 + src/ui/widget/text.h | 81 + src/ui/widget/tolerance-slider.cpp | 215 ++ src/ui/widget/tolerance-slider.h | 89 + src/ui/widget/unit-menu.cpp | 152 + src/ui/widget/unit-menu.h | 146 + src/ui/widget/unit-tracker.cpp | 294 ++ src/ui/widget/unit-tracker.h | 87 + 498 files changed, 155798 insertions(+) create mode 100644 src/ui/CMakeLists.txt create mode 100644 src/ui/README create mode 100644 src/ui/cache/README create mode 100644 src/ui/cache/svg_preview_cache.cpp create mode 100644 src/ui/cache/svg_preview_cache.h create mode 100644 src/ui/clipboard.cpp create mode 100644 src/ui/clipboard.h create mode 100644 src/ui/contextmenu.cpp create mode 100644 src/ui/contextmenu.h create mode 100644 src/ui/control-manager.cpp create mode 100644 src/ui/control-manager.h create mode 100644 src/ui/control-types.h create mode 100644 src/ui/desktop/README create mode 100644 src/ui/desktop/menubar.cpp create mode 100644 src/ui/desktop/menubar.h create mode 100644 src/ui/dialog-events.cpp create mode 100644 src/ui/dialog-events.h create mode 100644 src/ui/dialog/aboutbox.cpp create mode 100644 src/ui/dialog/aboutbox.h create mode 100644 src/ui/dialog/align-and-distribute.cpp create mode 100644 src/ui/dialog/align-and-distribute.h create mode 100644 src/ui/dialog/arrange-tab.h create mode 100644 src/ui/dialog/attrdialog.cpp create mode 100644 src/ui/dialog/attrdialog.h create mode 100644 src/ui/dialog/behavior.h create mode 100644 src/ui/dialog/calligraphic-profile-rename.cpp create mode 100644 src/ui/dialog/calligraphic-profile-rename.h create mode 100644 src/ui/dialog/clonetiler.cpp create mode 100644 src/ui/dialog/clonetiler.h create mode 100644 src/ui/dialog/color-item.cpp create mode 100644 src/ui/dialog/color-item.h create mode 100644 src/ui/dialog/debug.cpp create mode 100644 src/ui/dialog/debug.h create mode 100644 src/ui/dialog/desktop-tracker.cpp create mode 100644 src/ui/dialog/desktop-tracker.h create mode 100644 src/ui/dialog/dialog-manager.cpp create mode 100644 src/ui/dialog/dialog-manager.h create mode 100644 src/ui/dialog/dialog.cpp create mode 100644 src/ui/dialog/dialog.h create mode 100644 src/ui/dialog/dock-behavior.cpp create mode 100644 src/ui/dialog/dock-behavior.h create mode 100644 src/ui/dialog/document-metadata.cpp create mode 100644 src/ui/dialog/document-metadata.h create mode 100644 src/ui/dialog/document-properties.cpp create mode 100644 src/ui/dialog/document-properties.h create mode 100644 src/ui/dialog/export.cpp create mode 100644 src/ui/dialog/export.h create mode 100644 src/ui/dialog/extension-editor.cpp create mode 100644 src/ui/dialog/extension-editor.h create mode 100644 src/ui/dialog/extensions.cpp create mode 100644 src/ui/dialog/extensions.h create mode 100644 src/ui/dialog/filedialog.cpp create mode 100644 src/ui/dialog/filedialog.h create mode 100644 src/ui/dialog/filedialogimpl-gtkmm.cpp create mode 100644 src/ui/dialog/filedialogimpl-gtkmm.h create mode 100644 src/ui/dialog/filedialogimpl-win32.cpp create mode 100644 src/ui/dialog/filedialogimpl-win32.h create mode 100644 src/ui/dialog/fill-and-stroke.cpp create mode 100644 src/ui/dialog/fill-and-stroke.h create mode 100644 src/ui/dialog/filter-editor.cpp create mode 100644 src/ui/dialog/filter-editor.h create mode 100644 src/ui/dialog/filter-effects-dialog.cpp create mode 100644 src/ui/dialog/filter-effects-dialog.h create mode 100644 src/ui/dialog/find.cpp create mode 100644 src/ui/dialog/find.h create mode 100644 src/ui/dialog/floating-behavior.cpp create mode 100644 src/ui/dialog/floating-behavior.h create mode 100644 src/ui/dialog/font-substitution.cpp create mode 100644 src/ui/dialog/font-substitution.h create mode 100644 src/ui/dialog/glyphs.cpp create mode 100644 src/ui/dialog/glyphs.h create mode 100644 src/ui/dialog/grid-arrange-tab.cpp create mode 100644 src/ui/dialog/grid-arrange-tab.h create mode 100644 src/ui/dialog/guides.cpp create mode 100644 src/ui/dialog/guides.h create mode 100644 src/ui/dialog/icon-preview.cpp create mode 100644 src/ui/dialog/icon-preview.h create mode 100644 src/ui/dialog/inkscape-preferences.cpp create mode 100644 src/ui/dialog/inkscape-preferences.h create mode 100644 src/ui/dialog/input.cpp create mode 100644 src/ui/dialog/input.h create mode 100644 src/ui/dialog/knot-properties.cpp create mode 100644 src/ui/dialog/knot-properties.h create mode 100644 src/ui/dialog/layer-properties.cpp create mode 100644 src/ui/dialog/layer-properties.h create mode 100644 src/ui/dialog/layers.cpp create mode 100644 src/ui/dialog/layers.h create mode 100644 src/ui/dialog/livepatheffect-add.cpp create mode 100644 src/ui/dialog/livepatheffect-add.h create mode 100644 src/ui/dialog/livepatheffect-editor.cpp create mode 100644 src/ui/dialog/livepatheffect-editor.h create mode 100644 src/ui/dialog/lpe-fillet-chamfer-properties.cpp create mode 100644 src/ui/dialog/lpe-fillet-chamfer-properties.h create mode 100644 src/ui/dialog/lpe-powerstroke-properties.cpp create mode 100644 src/ui/dialog/lpe-powerstroke-properties.h create mode 100644 src/ui/dialog/memory.cpp create mode 100644 src/ui/dialog/memory.h create mode 100644 src/ui/dialog/messages.cpp create mode 100644 src/ui/dialog/messages.h create mode 100644 src/ui/dialog/new-from-template.cpp create mode 100644 src/ui/dialog/new-from-template.h create mode 100644 src/ui/dialog/object-attributes.cpp create mode 100644 src/ui/dialog/object-attributes.h create mode 100644 src/ui/dialog/object-properties.cpp create mode 100644 src/ui/dialog/object-properties.h create mode 100644 src/ui/dialog/objects.cpp create mode 100644 src/ui/dialog/objects.h create mode 100644 src/ui/dialog/paint-servers.cpp create mode 100644 src/ui/dialog/paint-servers.h create mode 100644 src/ui/dialog/panel-dialog.h create mode 100644 src/ui/dialog/polar-arrange-tab.cpp create mode 100644 src/ui/dialog/polar-arrange-tab.h create mode 100644 src/ui/dialog/print-colors-preview-dialog.cpp create mode 100644 src/ui/dialog/print-colors-preview-dialog.h create mode 100644 src/ui/dialog/print.cpp create mode 100644 src/ui/dialog/print.h create mode 100644 src/ui/dialog/save-template-dialog.cpp create mode 100644 src/ui/dialog/save-template-dialog.h create mode 100644 src/ui/dialog/selectorsdialog.cpp create mode 100644 src/ui/dialog/selectorsdialog.h create mode 100644 src/ui/dialog/spellcheck.cpp create mode 100644 src/ui/dialog/spellcheck.h create mode 100644 src/ui/dialog/styledialog.cpp create mode 100644 src/ui/dialog/styledialog.h create mode 100644 src/ui/dialog/svg-fonts-dialog.cpp create mode 100644 src/ui/dialog/svg-fonts-dialog.h create mode 100644 src/ui/dialog/svg-preview.cpp create mode 100644 src/ui/dialog/svg-preview.h create mode 100644 src/ui/dialog/swatches.cpp create mode 100644 src/ui/dialog/swatches.h create mode 100644 src/ui/dialog/symbols.cpp create mode 100644 src/ui/dialog/symbols.h create mode 100644 src/ui/dialog/tags.cpp create mode 100644 src/ui/dialog/tags.h create mode 100644 src/ui/dialog/template-load-tab.cpp create mode 100644 src/ui/dialog/template-load-tab.h create mode 100644 src/ui/dialog/template-widget.cpp create mode 100644 src/ui/dialog/template-widget.h create mode 100644 src/ui/dialog/text-edit.cpp create mode 100644 src/ui/dialog/text-edit.h create mode 100644 src/ui/dialog/tile.cpp create mode 100644 src/ui/dialog/tile.h create mode 100644 src/ui/dialog/tracedialog.cpp create mode 100644 src/ui/dialog/tracedialog.h create mode 100644 src/ui/dialog/transformation.cpp create mode 100644 src/ui/dialog/transformation.h create mode 100644 src/ui/dialog/undo-history.cpp create mode 100644 src/ui/dialog/undo-history.h create mode 100644 src/ui/dialog/xml-tree.cpp create mode 100644 src/ui/dialog/xml-tree.h create mode 100644 src/ui/drag-and-drop.cpp create mode 100644 src/ui/drag-and-drop.h create mode 100644 src/ui/draw-anchor.cpp create mode 100644 src/ui/draw-anchor.h create mode 100644 src/ui/event-debug.h create mode 100644 src/ui/icon-loader.cpp create mode 100644 src/ui/icon-loader.h create mode 100644 src/ui/icon-names.h create mode 100644 src/ui/interface.cpp create mode 100644 src/ui/interface.h create mode 100644 src/ui/monitor.cpp create mode 100644 src/ui/monitor.h create mode 100644 src/ui/pixmaps/README create mode 100644 src/ui/pixmaps/cursor-3dbox.xpm create mode 100644 src/ui/pixmaps/cursor-adj-a.xpm create mode 100644 src/ui/pixmaps/cursor-adj-h.xpm create mode 100644 src/ui/pixmaps/cursor-adj-l.xpm create mode 100644 src/ui/pixmaps/cursor-adj-s.xpm create mode 100644 src/ui/pixmaps/cursor-calligraphy.xpm create mode 100644 src/ui/pixmaps/cursor-connector.xpm create mode 100644 src/ui/pixmaps/cursor-crosshairs.xpm create mode 100644 src/ui/pixmaps/cursor-dropper-f.xpm create mode 100644 src/ui/pixmaps/cursor-dropper-s.xpm create mode 100644 src/ui/pixmaps/cursor-dropping-f.xpm create mode 100644 src/ui/pixmaps/cursor-dropping-s.xpm create mode 100644 src/ui/pixmaps/cursor-ellipse.xpm create mode 100644 src/ui/pixmaps/cursor-eraser.xpm create mode 100644 src/ui/pixmaps/cursor-gradient-add.xpm create mode 100644 src/ui/pixmaps/cursor-gradient.xpm create mode 100644 src/ui/pixmaps/cursor-measure.xpm create mode 100644 src/ui/pixmaps/cursor-node-d.xpm create mode 100644 src/ui/pixmaps/cursor-node.xpm create mode 100644 src/ui/pixmaps/cursor-paintbucket.xpm create mode 100644 src/ui/pixmaps/cursor-pen.xpm create mode 100644 src/ui/pixmaps/cursor-pencil.xpm create mode 100644 src/ui/pixmaps/cursor-rect.xpm create mode 100644 src/ui/pixmaps/cursor-select-d.xpm create mode 100644 src/ui/pixmaps/cursor-select-m.xpm create mode 100644 src/ui/pixmaps/cursor-select.xpm create mode 100644 src/ui/pixmaps/cursor-spiral.xpm create mode 100644 src/ui/pixmaps/cursor-spray-move.xpm create mode 100644 src/ui/pixmaps/cursor-spray.xpm create mode 100644 src/ui/pixmaps/cursor-star.xpm create mode 100644 src/ui/pixmaps/cursor-text-insert.xpm create mode 100644 src/ui/pixmaps/cursor-text.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-attract.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-color.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-less.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-more.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-in.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-jitter.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-out.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-push.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-repel.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-roughen.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-scale-down.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-scale-up.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-thicken.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-thin.xpm create mode 100644 src/ui/pixmaps/cursor-zoom-out.xpm create mode 100644 src/ui/pixmaps/cursor-zoom.xpm create mode 100644 src/ui/pixmaps/handles.xpm create mode 100644 src/ui/pref-pusher.cpp create mode 100644 src/ui/pref-pusher.h create mode 100644 src/ui/previewable.h create mode 100644 src/ui/previewholder.cpp create mode 100644 src/ui/previewholder.h create mode 100644 src/ui/selected-color.cpp create mode 100644 src/ui/selected-color.h create mode 100644 src/ui/shape-editor-knotholders.cpp create mode 100644 src/ui/shape-editor.cpp create mode 100644 src/ui/shape-editor.h create mode 100644 src/ui/simple-pref-pusher.cpp create mode 100644 src/ui/simple-pref-pusher.h create mode 100644 src/ui/tool-factory.cpp create mode 100644 src/ui/tool-factory.h create mode 100644 src/ui/tool/commit-events.h create mode 100644 src/ui/tool/control-point-selection.cpp create mode 100644 src/ui/tool/control-point-selection.h create mode 100644 src/ui/tool/control-point.cpp create mode 100644 src/ui/tool/control-point.h create mode 100644 src/ui/tool/curve-drag-point.cpp create mode 100644 src/ui/tool/curve-drag-point.h create mode 100644 src/ui/tool/event-utils.cpp create mode 100644 src/ui/tool/event-utils.h create mode 100644 src/ui/tool/manipulator.cpp create mode 100644 src/ui/tool/manipulator.h create mode 100644 src/ui/tool/modifier-tracker.cpp create mode 100644 src/ui/tool/modifier-tracker.h create mode 100644 src/ui/tool/multi-path-manipulator.cpp create mode 100644 src/ui/tool/multi-path-manipulator.h create mode 100644 src/ui/tool/node-types.h create mode 100644 src/ui/tool/node.cpp create mode 100644 src/ui/tool/node.h create mode 100644 src/ui/tool/path-manipulator.cpp create mode 100644 src/ui/tool/path-manipulator.h create mode 100644 src/ui/tool/selectable-control-point.cpp create mode 100644 src/ui/tool/selectable-control-point.h create mode 100644 src/ui/tool/selector.cpp create mode 100644 src/ui/tool/selector.h create mode 100644 src/ui/tool/shape-record.h create mode 100644 src/ui/tool/transform-handle-set.cpp create mode 100644 src/ui/tool/transform-handle-set.h create mode 100644 src/ui/toolbar/arc-toolbar.cpp create mode 100644 src/ui/toolbar/arc-toolbar.h create mode 100644 src/ui/toolbar/box3d-toolbar.cpp create mode 100644 src/ui/toolbar/box3d-toolbar.h create mode 100644 src/ui/toolbar/calligraphy-toolbar.cpp create mode 100644 src/ui/toolbar/calligraphy-toolbar.h create mode 100644 src/ui/toolbar/connector-toolbar.cpp create mode 100644 src/ui/toolbar/connector-toolbar.h create mode 100644 src/ui/toolbar/dropper-toolbar.cpp create mode 100644 src/ui/toolbar/dropper-toolbar.h create mode 100644 src/ui/toolbar/eraser-toolbar.cpp create mode 100644 src/ui/toolbar/eraser-toolbar.h create mode 100644 src/ui/toolbar/gradient-toolbar.cpp create mode 100644 src/ui/toolbar/gradient-toolbar.h create mode 100644 src/ui/toolbar/lpe-toolbar.cpp create mode 100644 src/ui/toolbar/lpe-toolbar.h create mode 100644 src/ui/toolbar/measure-toolbar.cpp create mode 100644 src/ui/toolbar/measure-toolbar.h create mode 100644 src/ui/toolbar/mesh-toolbar.cpp create mode 100644 src/ui/toolbar/mesh-toolbar.h create mode 100644 src/ui/toolbar/node-toolbar.cpp create mode 100644 src/ui/toolbar/node-toolbar.h create mode 100644 src/ui/toolbar/paintbucket-toolbar.cpp create mode 100644 src/ui/toolbar/paintbucket-toolbar.h create mode 100644 src/ui/toolbar/pencil-toolbar.cpp create mode 100644 src/ui/toolbar/pencil-toolbar.h create mode 100644 src/ui/toolbar/rect-toolbar.cpp create mode 100644 src/ui/toolbar/rect-toolbar.h create mode 100644 src/ui/toolbar/select-toolbar.cpp create mode 100644 src/ui/toolbar/select-toolbar.h create mode 100644 src/ui/toolbar/snap-toolbar.cpp create mode 100644 src/ui/toolbar/snap-toolbar.h create mode 100644 src/ui/toolbar/spiral-toolbar.cpp create mode 100644 src/ui/toolbar/spiral-toolbar.h create mode 100644 src/ui/toolbar/spray-toolbar.cpp create mode 100644 src/ui/toolbar/spray-toolbar.h create mode 100644 src/ui/toolbar/star-toolbar.cpp create mode 100644 src/ui/toolbar/star-toolbar.h create mode 100644 src/ui/toolbar/text-toolbar.cpp create mode 100644 src/ui/toolbar/text-toolbar.h create mode 100644 src/ui/toolbar/toolbar.cpp create mode 100644 src/ui/toolbar/toolbar.h create mode 100644 src/ui/toolbar/tweak-toolbar.cpp create mode 100644 src/ui/toolbar/tweak-toolbar.h create mode 100644 src/ui/toolbar/zoom-toolbar.cpp create mode 100644 src/ui/toolbar/zoom-toolbar.h create mode 100644 src/ui/tools-switch.cpp create mode 100644 src/ui/tools-switch.h create mode 100644 src/ui/tools/arc-tool.cpp create mode 100644 src/ui/tools/arc-tool.h create mode 100644 src/ui/tools/box3d-tool.cpp create mode 100644 src/ui/tools/box3d-tool.h create mode 100644 src/ui/tools/calligraphic-tool.cpp create mode 100644 src/ui/tools/calligraphic-tool.h create mode 100644 src/ui/tools/connector-tool.cpp create mode 100644 src/ui/tools/connector-tool.h create mode 100644 src/ui/tools/dropper-tool.cpp create mode 100644 src/ui/tools/dropper-tool.h create mode 100644 src/ui/tools/dynamic-base.cpp create mode 100644 src/ui/tools/dynamic-base.h create mode 100644 src/ui/tools/eraser-tool.cpp create mode 100644 src/ui/tools/eraser-tool.h create mode 100644 src/ui/tools/flood-tool.cpp create mode 100644 src/ui/tools/flood-tool.h create mode 100644 src/ui/tools/freehand-base.cpp create mode 100644 src/ui/tools/freehand-base.h create mode 100644 src/ui/tools/gradient-tool.cpp create mode 100644 src/ui/tools/gradient-tool.h create mode 100644 src/ui/tools/lpe-tool.cpp create mode 100644 src/ui/tools/lpe-tool.h create mode 100644 src/ui/tools/measure-tool.cpp create mode 100644 src/ui/tools/measure-tool.h create mode 100644 src/ui/tools/mesh-tool.cpp create mode 100644 src/ui/tools/mesh-tool.h create mode 100644 src/ui/tools/node-tool.cpp create mode 100644 src/ui/tools/node-tool.h create mode 100644 src/ui/tools/pen-tool.cpp create mode 100644 src/ui/tools/pen-tool.h create mode 100644 src/ui/tools/pencil-tool.cpp create mode 100644 src/ui/tools/pencil-tool.h create mode 100644 src/ui/tools/rect-tool.cpp create mode 100644 src/ui/tools/rect-tool.h create mode 100644 src/ui/tools/select-tool.cpp create mode 100644 src/ui/tools/select-tool.h create mode 100644 src/ui/tools/spiral-tool.cpp create mode 100644 src/ui/tools/spiral-tool.h create mode 100644 src/ui/tools/spray-tool.cpp create mode 100644 src/ui/tools/spray-tool.h create mode 100644 src/ui/tools/star-tool.cpp create mode 100644 src/ui/tools/star-tool.h create mode 100644 src/ui/tools/text-tool.cpp create mode 100644 src/ui/tools/text-tool.h create mode 100644 src/ui/tools/tool-base.cpp create mode 100644 src/ui/tools/tool-base.h create mode 100644 src/ui/tools/tweak-tool.cpp create mode 100644 src/ui/tools/tweak-tool.h create mode 100644 src/ui/tools/zoom-tool.cpp create mode 100644 src/ui/tools/zoom-tool.h create mode 100644 src/ui/util.cpp create mode 100644 src/ui/util.h create mode 100644 src/ui/uxmanager.cpp create mode 100644 src/ui/uxmanager.h create mode 100644 src/ui/view/README create mode 100644 src/ui/view/edit-widget-interface.h create mode 100644 src/ui/view/svg-view-widget.cpp create mode 100644 src/ui/view/svg-view-widget.h create mode 100644 src/ui/view/view-widget.cpp create mode 100644 src/ui/view/view-widget.h create mode 100644 src/ui/view/view.cpp create mode 100644 src/ui/view/view.h create mode 100644 src/ui/widget/alignment-selector.cpp create mode 100644 src/ui/widget/alignment-selector.h create mode 100644 src/ui/widget/anchor-selector.cpp create mode 100644 src/ui/widget/anchor-selector.h create mode 100644 src/ui/widget/attr-widget.h create mode 100644 src/ui/widget/button.cpp create mode 100644 src/ui/widget/button.h create mode 100644 src/ui/widget/clipmaskicon.cpp create mode 100644 src/ui/widget/clipmaskicon.h create mode 100644 src/ui/widget/color-entry.cpp create mode 100644 src/ui/widget/color-entry.h create mode 100644 src/ui/widget/color-icc-selector.cpp create mode 100644 src/ui/widget/color-icc-selector.h create mode 100644 src/ui/widget/color-notebook.cpp create mode 100644 src/ui/widget/color-notebook.h create mode 100644 src/ui/widget/color-picker.cpp create mode 100644 src/ui/widget/color-picker.h create mode 100644 src/ui/widget/color-preview.cpp create mode 100644 src/ui/widget/color-preview.h create mode 100644 src/ui/widget/color-scales.cpp create mode 100644 src/ui/widget/color-scales.h create mode 100644 src/ui/widget/color-slider.cpp create mode 100644 src/ui/widget/color-slider.h create mode 100644 src/ui/widget/color-wheel-selector.cpp create mode 100644 src/ui/widget/color-wheel-selector.h create mode 100644 src/ui/widget/combo-box-entry-tool-item.cpp create mode 100644 src/ui/widget/combo-box-entry-tool-item.h create mode 100644 src/ui/widget/combo-enums.h create mode 100644 src/ui/widget/combo-tool-item.cpp create mode 100644 src/ui/widget/combo-tool-item.h create mode 100644 src/ui/widget/dash-selector.cpp create mode 100644 src/ui/widget/dash-selector.h create mode 100644 src/ui/widget/dock-item.cpp create mode 100644 src/ui/widget/dock-item.h create mode 100644 src/ui/widget/dock.cpp create mode 100644 src/ui/widget/dock.h create mode 100644 src/ui/widget/entity-entry.cpp create mode 100644 src/ui/widget/entity-entry.h create mode 100644 src/ui/widget/entry.cpp create mode 100644 src/ui/widget/entry.h create mode 100644 src/ui/widget/filter-effect-chooser.cpp create mode 100644 src/ui/widget/filter-effect-chooser.h create mode 100644 src/ui/widget/font-button.cpp create mode 100644 src/ui/widget/font-button.h create mode 100644 src/ui/widget/font-selector-toolbar.cpp create mode 100644 src/ui/widget/font-selector-toolbar.h create mode 100644 src/ui/widget/font-selector.cpp create mode 100644 src/ui/widget/font-selector.h create mode 100644 src/ui/widget/font-variants.cpp create mode 100644 src/ui/widget/font-variants.h create mode 100644 src/ui/widget/font-variations.cpp create mode 100644 src/ui/widget/font-variations.h create mode 100644 src/ui/widget/frame.cpp create mode 100644 src/ui/widget/frame.h create mode 100644 src/ui/widget/highlight-picker.cpp create mode 100644 src/ui/widget/highlight-picker.h create mode 100644 src/ui/widget/iconrenderer.cpp create mode 100644 src/ui/widget/iconrenderer.h create mode 100644 src/ui/widget/imagetoggler.cpp create mode 100644 src/ui/widget/imagetoggler.h create mode 100644 src/ui/widget/ink-color-wheel.cpp create mode 100644 src/ui/widget/ink-color-wheel.h create mode 100644 src/ui/widget/ink-flow-box.cpp create mode 100644 src/ui/widget/ink-flow-box.h create mode 100644 src/ui/widget/ink-ruler.cpp create mode 100644 src/ui/widget/ink-ruler.h create mode 100644 src/ui/widget/ink-spinscale.cpp create mode 100644 src/ui/widget/ink-spinscale.h create mode 100644 src/ui/widget/insertordericon.cpp create mode 100644 src/ui/widget/insertordericon.h create mode 100644 src/ui/widget/label-tool-item.cpp create mode 100644 src/ui/widget/label-tool-item.h create mode 100644 src/ui/widget/labelled.cpp create mode 100644 src/ui/widget/labelled.h create mode 100644 src/ui/widget/layer-selector.cpp create mode 100644 src/ui/widget/layer-selector.h create mode 100644 src/ui/widget/layertypeicon.cpp create mode 100644 src/ui/widget/layertypeicon.h create mode 100644 src/ui/widget/licensor.cpp create mode 100644 src/ui/widget/licensor.h create mode 100644 src/ui/widget/notebook-page.cpp create mode 100644 src/ui/widget/notebook-page.h create mode 100644 src/ui/widget/object-composite-settings.cpp create mode 100644 src/ui/widget/object-composite-settings.h create mode 100644 src/ui/widget/page-sizer.cpp create mode 100644 src/ui/widget/page-sizer.h create mode 100644 src/ui/widget/pages-skeleton.h create mode 100644 src/ui/widget/panel.cpp create mode 100644 src/ui/widget/panel.h create mode 100644 src/ui/widget/point.cpp create mode 100644 src/ui/widget/point.h create mode 100644 src/ui/widget/preferences-widget.cpp create mode 100644 src/ui/widget/preferences-widget.h create mode 100644 src/ui/widget/preview.cpp create mode 100644 src/ui/widget/preview.h create mode 100644 src/ui/widget/random.cpp create mode 100644 src/ui/widget/random.h create mode 100644 src/ui/widget/registered-enums.h create mode 100644 src/ui/widget/registered-widget.cpp create mode 100644 src/ui/widget/registered-widget.h create mode 100644 src/ui/widget/registry.cpp create mode 100644 src/ui/widget/registry.h create mode 100644 src/ui/widget/rendering-options.cpp create mode 100644 src/ui/widget/rendering-options.h create mode 100644 src/ui/widget/rotateable.cpp create mode 100644 src/ui/widget/rotateable.h create mode 100644 src/ui/widget/scalar-unit.cpp create mode 100644 src/ui/widget/scalar-unit.h create mode 100644 src/ui/widget/scalar.cpp create mode 100644 src/ui/widget/scalar.h create mode 100644 src/ui/widget/selected-style.cpp create mode 100644 src/ui/widget/selected-style.h create mode 100644 src/ui/widget/spin-button-tool-item.cpp create mode 100644 src/ui/widget/spin-button-tool-item.h create mode 100644 src/ui/widget/spin-scale.cpp create mode 100644 src/ui/widget/spin-scale.h create mode 100644 src/ui/widget/spin-slider.cpp create mode 100644 src/ui/widget/spin-slider.h create mode 100644 src/ui/widget/spinbutton.cpp create mode 100644 src/ui/widget/spinbutton.h create mode 100644 src/ui/widget/style-subject.cpp create mode 100644 src/ui/widget/style-subject.h create mode 100644 src/ui/widget/style-swatch.cpp create mode 100644 src/ui/widget/style-swatch.h create mode 100644 src/ui/widget/text.cpp create mode 100644 src/ui/widget/text.h create mode 100644 src/ui/widget/tolerance-slider.cpp create mode 100644 src/ui/widget/tolerance-slider.h create mode 100644 src/ui/widget/unit-menu.cpp create mode 100644 src/ui/widget/unit-menu.h create mode 100644 src/ui/widget/unit-tracker.cpp create mode 100644 src/ui/widget/unit-tracker.h (limited to 'src/ui') diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt new file mode 100644 index 0000000..de23597 --- /dev/null +++ b/src/ui/CMakeLists.txt @@ -0,0 +1,477 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(ui_SRC + clipboard.cpp + contextmenu.cpp + control-manager.cpp + dialog-events.cpp + draw-anchor.cpp + drag-and-drop.cpp + icon-loader.cpp + interface.cpp + monitor.cpp + pref-pusher.cpp + previewholder.cpp + selected-color.cpp + shape-editor.cpp + shape-editor-knotholders.cpp + simple-pref-pusher.cpp + tool-factory.cpp + tools-switch.cpp + util.cpp + uxmanager.cpp + + cache/svg_preview_cache.cpp + + desktop/menubar.cpp + + tool/control-point-selection.cpp + tool/control-point.cpp + tool/curve-drag-point.cpp + tool/event-utils.cpp + tool/manipulator.cpp + tool/modifier-tracker.cpp + tool/multi-path-manipulator.cpp + tool/node.cpp + tool/path-manipulator.cpp + tool/selectable-control-point.cpp + tool/selector.cpp + tool/transform-handle-set.cpp + + toolbar/arc-toolbar.cpp + toolbar/box3d-toolbar.cpp + toolbar/calligraphy-toolbar.cpp + toolbar/connector-toolbar.cpp + toolbar/dropper-toolbar.cpp + toolbar/eraser-toolbar.cpp + toolbar/gradient-toolbar.cpp + toolbar/lpe-toolbar.cpp + toolbar/measure-toolbar.cpp + toolbar/mesh-toolbar.cpp + toolbar/node-toolbar.cpp + toolbar/paintbucket-toolbar.cpp + toolbar/pencil-toolbar.cpp + toolbar/rect-toolbar.cpp + toolbar/select-toolbar.cpp + toolbar/snap-toolbar.cpp + toolbar/spiral-toolbar.cpp + toolbar/spray-toolbar.cpp + toolbar/star-toolbar.cpp + toolbar/text-toolbar.cpp + toolbar/toolbar.cpp + toolbar/tweak-toolbar.cpp + toolbar/zoom-toolbar.cpp + + tools/arc-tool.cpp + tools/box3d-tool.cpp + tools/calligraphic-tool.cpp + tools/connector-tool.cpp + tools/dropper-tool.cpp + tools/dynamic-base.cpp + tools/eraser-tool.cpp + tools/flood-tool.cpp + tools/freehand-base.cpp + tools/gradient-tool.cpp + tools/lpe-tool.cpp + tools/measure-tool.cpp + tools/mesh-tool.cpp + tools/node-tool.cpp + tools/pencil-tool.cpp + tools/pen-tool.cpp + tools/rect-tool.cpp + tools/select-tool.cpp + tools/spiral-tool.cpp + tools/spray-tool.cpp + tools/star-tool.cpp + tools/text-tool.cpp + tools/tool-base.cpp + tools/tweak-tool.cpp + tools/zoom-tool.cpp + + dialog/aboutbox.cpp + dialog/align-and-distribute.cpp + dialog/calligraphic-profile-rename.cpp + dialog/clonetiler.cpp + dialog/color-item.cpp + dialog/attrdialog.cpp + dialog/debug.cpp + dialog/desktop-tracker.cpp + dialog/dialog-manager.cpp + dialog/dialog.cpp + dialog/dock-behavior.cpp + dialog/document-metadata.cpp + dialog/document-properties.cpp + dialog/export.cpp + dialog/extension-editor.cpp + dialog/extensions.cpp + dialog/filedialog.cpp + dialog/filedialogimpl-gtkmm.cpp + dialog/fill-and-stroke.cpp + dialog/filter-editor.cpp + dialog/filter-effects-dialog.cpp + dialog/find.cpp + dialog/floating-behavior.cpp + dialog/font-substitution.cpp + dialog/glyphs.cpp + dialog/grid-arrange-tab.cpp + dialog/guides.cpp + dialog/icon-preview.cpp + dialog/inkscape-preferences.cpp + dialog/input.cpp + dialog/knot-properties.cpp + dialog/layer-properties.cpp + dialog/layers.cpp + dialog/livepatheffect-add.cpp + dialog/livepatheffect-editor.cpp + dialog/lpe-fillet-chamfer-properties.cpp + dialog/lpe-powerstroke-properties.cpp + dialog/memory.cpp + dialog/messages.cpp + dialog/new-from-template.cpp + dialog/object-attributes.cpp + dialog/object-properties.cpp + dialog/objects.cpp + dialog/polar-arrange-tab.cpp + dialog/print-colors-preview-dialog.cpp + dialog/print.cpp + dialog/selectorsdialog.cpp + dialog/styledialog.cpp + dialog/svg-fonts-dialog.cpp + dialog/svg-preview.cpp + dialog/swatches.cpp + dialog/symbols.cpp + dialog/paint-servers.cpp + dialog/tags.cpp + dialog/template-load-tab.cpp + dialog/template-widget.cpp + dialog/text-edit.cpp + dialog/tile.cpp + dialog/tracedialog.cpp + dialog/transformation.cpp + dialog/undo-history.cpp + dialog/xml-tree.cpp + dialog/save-template-dialog.cpp + + widget/iconrenderer.cpp + widget/alignment-selector.cpp + widget/anchor-selector.cpp + widget/button.cpp + widget/clipmaskicon.cpp + widget/color-entry.cpp + widget/color-icc-selector.cpp + widget/color-notebook.cpp + widget/color-picker.cpp + widget/color-preview.cpp icon-loader.cpp + widget/color-scales.cpp + widget/color-slider.cpp + widget/color-wheel-selector.cpp + widget/combo-box-entry-tool-item.cpp + widget/combo-tool-item.cpp + widget/dash-selector.cpp + widget/dock-item.cpp + widget/dock.cpp + widget/entity-entry.cpp + widget/entry.cpp + widget/filter-effect-chooser.cpp + widget/font-button.cpp + widget/font-selector.cpp + widget/font-selector-toolbar.cpp + widget/font-variants.cpp + widget/font-variations.cpp + widget/frame.cpp + widget/highlight-picker.cpp + widget/imagetoggler.cpp + widget/ink-color-wheel.cpp + widget/ink-flow-box.cpp + widget/ink-ruler.cpp + widget/ink-spinscale.cpp + widget/insertordericon.cpp + widget/label-tool-item.cpp + widget/labelled.cpp + widget/layer-selector.cpp + widget/layertypeicon.cpp + widget/licensor.cpp + widget/notebook-page.cpp + widget/object-composite-settings.cpp + widget/page-sizer.cpp + widget/panel.cpp + widget/point.cpp + widget/preferences-widget.cpp + widget/preview.cpp + widget/random.cpp + widget/registered-widget.cpp + widget/registry.cpp + widget/rendering-options.cpp + widget/rotateable.cpp + widget/scalar-unit.cpp + widget/scalar.cpp + widget/selected-style.cpp + widget/spin-button-tool-item.cpp + widget/spin-scale.cpp + widget/spin-slider.cpp + widget/spinbutton.cpp + widget/style-subject.cpp + widget/style-swatch.cpp + widget/text.cpp + widget/tolerance-slider.cpp + widget/unit-menu.cpp + widget/unit-tracker.cpp + + view/svg-view-widget.cpp + view/view.cpp + view/view-widget.cpp + + + # ------- + # Headers + clipboard.h + contextmenu.h + control-manager.h + control-types.h + dialog-events.h + drag-and-drop.h + draw-anchor.h + event-debug.h + icon-names.h + icon-loader.h + interface.h + monitor.h + pref-pusher.h + previewable.h + previewholder.h + selected-color.h + shape-editor.h + simple-pref-pusher.h + tool-factory.h + tools-switch.h + util.h + uxmanager.h + + cache/svg_preview_cache.h + + desktop/menubar.cpp + + dialog/aboutbox.h + dialog/align-and-distribute.h + dialog/arrange-tab.h + dialog/behavior.h + dialog/calligraphic-profile-rename.h + dialog/clonetiler.h + dialog/color-item.h + dialog/attrdialog.h + dialog/debug.h + dialog/desktop-tracker.h + dialog/dialog-manager.h + dialog/dialog.h + dialog/dock-behavior.h + dialog/document-metadata.h + dialog/document-properties.h + dialog/export.h + dialog/extension-editor.h + dialog/extensions.h + dialog/filedialog.h + dialog/filedialogimpl-gtkmm.h + dialog/filedialogimpl-win32.h + dialog/fill-and-stroke.h + dialog/filter-editor.h + dialog/filter-effects-dialog.h + dialog/find.h + dialog/floating-behavior.h + dialog/font-substitution.h + dialog/glyphs.h + dialog/grid-arrange-tab.h + dialog/guides.h + dialog/icon-preview.h + dialog/inkscape-preferences.h + dialog/input.h + dialog/knot-properties.h + dialog/layer-properties.h + dialog/layers.h + dialog/livepatheffect-add.h + dialog/livepatheffect-editor.h + dialog/lpe-fillet-chamfer-properties.h + dialog/lpe-powerstroke-properties.h + dialog/memory.h + dialog/messages.h + dialog/new-from-template.h + dialog/object-attributes.h + dialog/object-properties.h + dialog/objects.h + dialog/panel-dialog.h + dialog/polar-arrange-tab.h + dialog/print-colors-preview-dialog.h + dialog/print.h + dialog/selectorsdialog.h + dialog/styledialog.h + dialog/svg-fonts-dialog.h + dialog/svg-preview.h + dialog/swatches.h + dialog/symbols.h + dialog/paint-servers.h + dialog/tags.h + dialog/template-load-tab.h + dialog/template-widget.h + dialog/text-edit.h + dialog/tile.h + dialog/tracedialog.h + dialog/transformation.h + dialog/undo-history.h + dialog/xml-tree.h + dialog/save-template-dialog.h + + tool/commit-events.h + tool/control-point-selection.h + tool/control-point.h + tool/curve-drag-point.h + tool/event-utils.h + tool/manipulator.h + tool/modifier-tracker.h + tool/multi-path-manipulator.h + tool/node-types.h + tool/node.h + tool/path-manipulator.h + tool/selectable-control-point.h + tool/selector.h + tool/shape-record.h + tool/transform-handle-set.h + + toolbar/arc-toolbar.h + toolbar/box3d-toolbar.h + toolbar/calligraphy-toolbar.h + toolbar/connector-toolbar.h + toolbar/dropper-toolbar.h + toolbar/eraser-toolbar.h + toolbar/gradient-toolbar.h + toolbar/lpe-toolbar.h + toolbar/measure-toolbar.h + toolbar/mesh-toolbar.h + toolbar/node-toolbar.h + toolbar/paintbucket-toolbar.h + toolbar/pencil-toolbar.h + toolbar/rect-toolbar.h + toolbar/select-toolbar.h + toolbar/snap-toolbar.h + toolbar/spiral-toolbar.h + toolbar/spray-toolbar.h + toolbar/star-toolbar.h + toolbar/text-toolbar.h + toolbar/toolbar.h + toolbar/tweak-toolbar.h + toolbar/zoom-toolbar.h + + tools/arc-tool.h + tools/box3d-tool.h + tools/calligraphic-tool.h + tools/connector-tool.h + tools/dropper-tool.h + tools/dynamic-base.h + tools/eraser-tool.h + tools/flood-tool.h + tools/freehand-base.h + tools/gradient-tool.h + tools/lpe-tool.h + tools/measure-tool.h + tools/mesh-tool.h + tools/node-tool.h + tools/pen-tool.h + tools/pencil-tool.h + tools/rect-tool.h + tools/select-tool.h + tools/spiral-tool.h + tools/spray-tool.h + tools/star-tool.h + tools/text-tool.h + tools/tool-base.h + tools/tweak-tool.h + tools/zoom-tool.h + + widget/iconrenderer.h + widget/alignment-selector.h + widget/anchor-selector.h + widget/attr-widget.h + widget/button.h + widget/clipmaskicon.h + widget/color-entry.h + widget/color-icc-selector.h + widget/color-notebook.h + widget/color-picker.h + widget/color-preview.h + widget/color-scales.h + widget/color-slider.h + widget/color-wheel-selector.h + widget/combo-enums.h + widget/combo-box-entry-tool-item.h + widget/combo-tool-item.h + widget/dash-selector.h + widget/dock-item.h + widget/dock.h + widget/entity-entry.h + widget/entry.h + widget/filter-effect-chooser.h + widget/font-button.h + widget/font-selector.h + widget/font-selector-toolbar.h + widget/font-variants.h + widget/font-variations.h + widget/frame.h + widget/highlight-picker.h + widget/insertordericon.h + widget/imagetoggler.h + widget/ink-color-wheel.h + widget/ink-flow-box.h + widget/ink-ruler.h + widget/ink-spinscale.h + widget/label-tool-item.h + widget/labelled.h + widget/layer-selector.h + widget/layertypeicon.h + widget/licensor.h + widget/notebook-page.h + widget/object-composite-settings.h + widget/page-sizer.h + widget/pages-skeleton.h + widget/panel.h + widget/point.h + widget/preferences-widget.h + widget/preview.h + widget/random.h + widget/registered-enums.h + widget/registered-widget.h + widget/registry.h + widget/rendering-options.h + widget/rotateable.h + widget/scalar-unit.h + widget/scalar.h + widget/selected-style.h + widget/spin-button-tool-item.h + widget/spin-scale.h + widget/spin-slider.h + widget/spinbutton.h + widget/style-subject.h + widget/style-swatch.h + widget/text.h + widget/tolerance-slider.h + widget/unit-menu.h + widget/unit-tracker.h + + view/edit-widget-interface.h + view/svg-view-widget.h + view/view.h + view/view-widget.h +) + +if(WIN32) + list(APPEND ui_SRC + dialog/filedialogimpl-win32.cpp + ) +endif() + +add_inkscape_source("${ui_SRC}") + +set ( ui_spellcheck_SRC + dialog/spellcheck.cpp + dialog/spellcheck.h +) + +if ("${HAVE_ASPELL}") + add_inkscape_source("${ui_spellcheck_SRC}") +endif() diff --git a/src/ui/README b/src/ui/README new file mode 100644 index 0000000..3929271 --- /dev/null +++ b/src/ui/README @@ -0,0 +1,21 @@ + + +This directory contains all the code related to the Graphical User Interface. + +To do: + +* Move all GTK related code here. +* Better organize directories: + + - cache + - dialog + - menu + - onscreen + - toolbar + - tools + - view? + - widget + -- basic + -- composite + -- etc. + diff --git a/src/ui/cache/README b/src/ui/cache/README new file mode 100644 index 0000000..1cdbaee --- /dev/null +++ b/src/ui/cache/README @@ -0,0 +1,3 @@ +This directory is for utility code for maintaining caches of UI things, +such as symbol previews, thumbnails, font lists, brushes, dashes, etc. + diff --git a/src/ui/cache/svg_preview_cache.cpp b/src/ui/cache/svg_preview_cache.cpp new file mode 100644 index 0000000..d6a35fb --- /dev/null +++ b/src/ui/cache/svg_preview_cache.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVGPreview: Preview cache + */ +/* + * Authors: + * Lauris Kaplinski + * Bryce Harrington + * bulia byak + * + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include + +#include <2geom/transforms.h> + +#include "selection.h" +#include "inkscape.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing.h" + + +#include "ui/cache/svg_preview_cache.h" + +GdkPixbuf* render_pixbuf(Inkscape::Drawing &drawing, double scale_factor, Geom::Rect const &dbox, unsigned psize) +{ + drawing.root()->setTransform(Geom::Scale(scale_factor)); + + Geom::IntRect ibox = (dbox * Geom::Scale(scale_factor)).roundOutwards(); + + drawing.update(ibox); + + /* Find visible area */ + int width = ibox.width(); + int height = ibox.height(); + int dx = psize; + int dy = psize; + dx = (dx - width)/2; // watch out for size, since 'unsigned'-'signed' can cause problems if the result is negative + dy = (dy - height)/2; + + Geom::IntRect area = Geom::IntRect::from_xywh( + ibox.min() - Geom::IntPoint(dx, dy), Geom::IntPoint(psize, psize)); + + /* Render */ + cairo_surface_t *s = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, psize, psize); + Inkscape::DrawingContext dc(s, area.min()); + + drawing.render(dc, area, Inkscape::DrawingItem::RENDER_BYPASS_CACHE); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +namespace Inkscape { +namespace UI { +namespace Cache { + +SvgPreview::SvgPreview() += default; + +SvgPreview::~SvgPreview() +{ + for (auto & i : _pixmap_cache) + { + g_object_unref(i.second); + i.second = NULL; + } +} + +Glib::ustring SvgPreview::cache_key(gchar const *uri, gchar const *name, unsigned psize) const { + Glib::ustring key; + key += (uri!=nullptr) ? uri : ""; + key += ":"; + key += (name!=nullptr) ? name : "unknown"; + key += ":"; + key += psize; + return key; +} + +GdkPixbuf* SvgPreview::get_preview_from_cache(const Glib::ustring& key) { + std::map::iterator found = _pixmap_cache.find(key); + if ( found != _pixmap_cache.end() ) { + return found->second; + } + return nullptr; +} + +void SvgPreview::set_preview_in_cache(const Glib::ustring& key, GdkPixbuf* px) { + g_object_ref(px); + _pixmap_cache[key] = px; +} + +GdkPixbuf* SvgPreview::get_preview(const gchar* uri, const gchar* id, Inkscape::DrawingItem */*root*/, + double /*scale_factor*/, unsigned int psize) { + // First try looking up the cached preview in the cache map + Glib::ustring key = cache_key(uri, id, psize); + GdkPixbuf* px = get_preview_from_cache(key); + + if (px == nullptr) { + /* + px = render_pixbuf(root, scale_factor, dbox, psize); + set_preview_in_cache(key, px); + */ + } + return px; +} + +void SvgPreview::remove_preview_from_cache(const Glib::ustring& key) { + std::map::iterator found = _pixmap_cache.find(key); + if ( found != _pixmap_cache.end() ) { + g_object_unref(found->second); + found->second = NULL; + _pixmap_cache.erase(key); + } +} + + +} +} +} + +/* + 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 : diff --git a/src/ui/cache/svg_preview_cache.h b/src/ui/cache/svg_preview_cache.h new file mode 100644 index 0000000..19c4548 --- /dev/null +++ b/src/ui/cache/svg_preview_cache.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Preview cache + */ +/* + * Copyright (C) 2007 Bryce W. Harrington + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H +#define SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H + +#include +#include +#include +#include <2geom/rect.h> + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + + +GdkPixbuf* render_pixbuf(Inkscape::Drawing &drawing, double scale_factor, const Geom::Rect& dbox, unsigned psize); + +namespace Inkscape { +namespace UI { +namespace Cache { + +class SvgPreview { + protected: + std::map _pixmap_cache; + + public: + SvgPreview(); + ~SvgPreview(); + + Glib::ustring cache_key(gchar const *uri, gchar const *name, unsigned psize) const; + GdkPixbuf* get_preview_from_cache(const Glib::ustring& key); + void set_preview_in_cache(const Glib::ustring& key, GdkPixbuf* px); + GdkPixbuf* get_preview(const gchar* uri, const gchar* id, Inkscape::DrawingItem *root, double scale_factor, unsigned int psize); + void remove_preview_from_cache(const Glib::ustring& key); +}; + +}; // namespace Cache +}; // namespace UI +}; // namespace Inkscape + + + +#endif // SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H +/* + 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 : + + diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp new file mode 100644 index 0000000..0914293 --- /dev/null +++ b/src/ui/clipboard.cpp @@ -0,0 +1,1682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * System-wide clipboard management - implementation. + *//* + * Authors: + * see git history + * Krzysztof Kosiński + * Jon A. Cruz + * Incorporates some code from selection-chemistry.cpp, see that file for more credits. + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "ui/clipboard.h" + +// TODO: reduce header bloat if possible + +#include "file.h" // for file_import, used in _pasteImage +#include +#include // for g_file_set_contents etc., used in _onGet and paste +#include "inkgc/gc-core.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" +#include "inkscape.h" +#include "desktop.h" + +#include "desktop-style.h" // for sp_desktop_set_style, used in _pasteStyle +#include "document.h" +#include "message-stack.h" +#include "context-fns.h" +#include "ui/tools/dropper-tool.h" // used in copy() +#include "extension/db.h" // extension database +#include "extension/input.h" +#include "extension/output.h" +#include "selection-chemistry.h" +#include <2geom/transforms.h> +#include "gradient-drag.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/parameter/path.h" +#include "ui/tools/text-tool.h" +#include "text-editing.h" +#include "text-chemistry.h" +#include "ui/tools-switch.h" +#include "path-chemistry.h" +#include "util/units.h" +#include "helper/png-write.h" +#include "extension/find_extension_by_mime.h" + +#include "object/box3d.h" +#include "object/persp3d.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-hatch.h" +#include "object/sp-item-transform.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-pattern.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-flowtext.h" +#include "object/sp-textpath.h" +#include "object/sp-use.h" +#include "style.h" + +#include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection +#include "svg/css-ostringstream.h" // used in copy +#include "svg/svg-color.h" + +/// Made up mimetype to represent Gdk::Pixbuf clipboard contents. +#define CLIPBOARD_GDK_PIXBUF_TARGET "image/x-gdk-pixbuf" + +#define CLIPBOARD_TEXT_TARGET "text/plain" + +#ifdef _WIN32 +#include +#endif + +namespace Inkscape { +namespace UI { + + +/** + * Default implementation of the clipboard manager. + */ +class ClipboardManagerImpl : public ClipboardManager { +public: + void copy(ObjectSet *set) override; + void copyPathParameter(Inkscape::LivePathEffect::PathParam *) override; + void copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol) override; + bool paste(SPDesktop *desktop, bool in_place) override; + bool pasteStyle(ObjectSet *set) override; + bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) override; + bool pastePathEffect(ObjectSet *set) override; + Glib::ustring getPathParameter(SPDesktop* desktop) override; + Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) override; + std::vector getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) override; + const gchar *getFirstObjectID() override; + + ClipboardManagerImpl(); + ~ClipboardManagerImpl() override; + +private: + void _copySelection(ObjectSet *); + void _copyUsedDefs(SPItem *); + void _copyGradient(SPGradient *); + void _copyPattern(SPPattern *); + void _copyHatch(SPHatch *); + void _copyTextPath(SPTextPath *); + Inkscape::XML::Node *_copyNode(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + Inkscape::XML::Node *_copyIgnoreDup(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + + bool _pasteImage(SPDocument *doc); + bool _pasteText(SPDesktop *desktop); + void _applyPathEffect(SPItem *, gchar const *); + SPDocument *_retrieveClipboard(Glib::ustring = ""); + + // clipboard callbacks + void _onGet(Gtk::SelectionData &, guint); + void _onClear(); + + // various helpers + void _createInternalClipboard(); + void _discardInternalClipboard(); + Inkscape::XML::Node *_createClipNode(); + Geom::Scale _getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y); + Glib::ustring _getBestTarget(); + void _setClipboardTargets(); + void _setClipboardColor(guint32); + void _userWarn(SPDesktop *, char const *); + + // private properites + SPDocument *_clipboardSPDoc; ///< Document that stores the clipboard until someone requests it + Inkscape::XML::Node *_defs; ///< Reference to the clipboard document's defs node + Inkscape::XML::Node *_root; ///< Reference to the clipboard's root node + Inkscape::XML::Node *_clipnode; ///< The node that holds extra information + Inkscape::XML::Document *_doc; ///< Reference to the clipboard's Inkscape::XML::Document + std::set cloned_elements; + std::vector te_selected_style; + std::vector te_selected_style_positions; + int nr_blocks = 0; + unsigned copied_style_length = 0; + + + // we need a way to copy plain text AND remember its style; + // the standard _clipnode is only available in an SVG tree, hence this special storage + SPCSSAttr *_text_style; ///< Style copied along with plain text fragment + + Glib::RefPtr _clipboard; ///< Handle to the system wide clipboard - for convenience + std::list _preferred_targets; ///< List of supported clipboard targets +}; + + +ClipboardManagerImpl::ClipboardManagerImpl() + : _clipboardSPDoc(nullptr), + _defs(nullptr), + _root(nullptr), + _clipnode(nullptr), + _doc(nullptr), + _text_style(nullptr), + _clipboard( Gtk::Clipboard::get() ) +{ + // Clipboard Formats: http://msdn.microsoft.com/en-us/library/ms649013(VS.85).aspx + // On Windows, most graphical applications can handle CF_DIB/CF_BITMAP and/or CF_ENHMETAFILE + // GTK automatically presents an "image/bmp" target as CF_DIB/CF_BITMAP + // Presenting "image/x-emf" as CF_ENHMETAFILE must be done by Inkscape ? + + // push supported clipboard targets, in order of preference + _preferred_targets.emplace_back("image/x-inkscape-svg"); + _preferred_targets.emplace_back("image/svg+xml"); + _preferred_targets.emplace_back("image/svg+xml-compressed"); + _preferred_targets.emplace_back("image/x-emf"); + _preferred_targets.emplace_back("CF_ENHMETAFILE"); + _preferred_targets.emplace_back("WCF_ENHMETAFILE"); // seen on Wine + _preferred_targets.emplace_back("application/pdf"); + _preferred_targets.emplace_back("image/x-adobe-illustrator"); + + // Clipboard requests on app termination can cause undesired extension + // popup windows. Clearing the clipboard can prevent this. + auto application = Gio::Application::get_default(); + if (application) { + application->signal_shutdown().connect_notify([this]() { this->_discardInternalClipboard(); }); + } +} + + +ClipboardManagerImpl::~ClipboardManagerImpl() = default; + + +/** + * Copy selection contents to the clipboard. + */ +void ClipboardManagerImpl::copy(ObjectSet *set) +{ + if ( set->desktop() ) { + SPDesktop *desktop = set->desktop(); + + // Special case for when the gradient dragger is active - copies gradient color + if (desktop->event_context->get_drag()) { + GrDrag *drag = desktop->event_context->get_drag(); + if (drag->hasSelection()) { + guint32 col = drag->getColor(); + + // set the color as clipboard content (text in RRGGBBAA format) + _setClipboardColor(col); + + // create a style with this color on fill and opacity in master opacity, so it can be + // pasted on other stops or objects + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = sp_repr_css_attr_new(); + // print and set properties + gchar color_str[16]; + g_snprintf(color_str, 16, "#%06x", col >> 8); + sp_repr_css_set_property(_text_style, "fill", color_str); + float opacity = SP_RGBA32_A_F(col); + if (opacity > 1.0) { + opacity = 1.0; // safeguard + } + Inkscape::CSSOStringStream opcss; + opcss << opacity; + sp_repr_css_set_property(_text_style, "opacity", opcss.str().data()); + + _discardInternalClipboard(); + return; + } + } + + // Special case for when the color picker ("dropper") is active - copies color under cursor + if (tools_isactive(desktop, TOOLS_DROPPER)) { + //_setClipboardColor(sp_dropper_context_get_color(desktop->event_context)); + _setClipboardColor(SP_DROPPER_CONTEXT(desktop->event_context)->get_color()); + _discardInternalClipboard(); + return; + } + + // Special case for when the text tool is active - if some text is selected, copy plain text, + // not the object that holds it; also copy the style at cursor into + if (tools_isactive(desktop, TOOLS_TEXT)) { + _discardInternalClipboard(); + Glib::ustring selected_text = Inkscape::UI::Tools::sp_text_get_selected_text(desktop->event_context); + _clipboard->set_text(selected_text); + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = Inkscape::UI::Tools::sp_text_get_style_at_cursor(desktop->event_context); + return; + } + } + if (set->isEmpty()) { // check whether something is selected + _userWarn(set->desktop(), _("Nothing was copied.")); + return; + } + _discardInternalClipboard(); + + _createInternalClipboard(); // construct a new clipboard document + _copySelection(set); // copy all items in the selection to the internal clipboard + fit_canvas_to_drawing(_clipboardSPDoc); + + _setClipboardTargets(); +} + + +/** + * Copy a Live Path Effect path parameter to the clipboard. + * @param pp The path parameter to store in the clipboard. + */ +void ClipboardManagerImpl::copyPathParameter(Inkscape::LivePathEffect::PathParam *pp) +{ + if ( pp == nullptr ) { + return; + } + gchar *svgd = sp_svg_write_path( pp->get_pathvector() ); + if ( svgd == nullptr || *svgd == '\0' ) { + return; + } + + _discardInternalClipboard(); + _createInternalClipboard(); + + Inkscape::XML::Node *pathnode = _doc->createElement("svg:path"); + pathnode->setAttribute("d", svgd); + g_free(svgd); + _root->appendChild(pathnode); + Inkscape::GC::release(pathnode); + + fit_canvas_to_drawing(_clipboardSPDoc); + _setClipboardTargets(); +} + +/** + * Copy a symbol from the symbol dialog. + * @param symbol The Inkscape::XML::Node for the symbol. + */ +void ClipboardManagerImpl::copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol) +{ + //std::cout << "ClipboardManagerImpl::copySymbol" << std::endl; + if ( symbol == nullptr ) { + return; + } + + _discardInternalClipboard(); + _createInternalClipboard(); + + // We add "_duplicate" to have a well defined symbol name that + // bypasses the "prevent_id_classes" routine. We'll get rid of it + // when we paste. + Inkscape::XML::Node *repr = symbol->duplicate(_doc); + Glib::ustring symbol_name = repr->attribute("id"); + + symbol_name += "_inkscape_duplicate"; + repr->setAttribute("id", symbol_name); + _defs->appendChild(repr); + + Glib::ustring id("#"); + id += symbol->attribute("id"); + + gdouble scale_units = 1; // scale from "px" to "document-units" + Inkscape::XML::Node *nv_repr = SP_ACTIVE_DESKTOP->getNamedView()->getRepr(); + if (nv_repr->attribute("inkscape:document-units")) + scale_units = Inkscape::Util::Quantity::convert(1, "px", nv_repr->attribute("inkscape:document-units")); + SPObject *cmobj = _clipboardSPDoc->getObjectByRepr(repr); + if (cmobj && !user_symbol) { // convert only stock symbols + if (!Geom::are_near(scale_units, 1.0, Geom::EPSILON)) { + dynamic_cast(cmobj)->scaleChildItemsRec( + Geom::Scale(scale_units), Geom::Point(0, SP_ACTIVE_DESKTOP->getDocument()->getHeight().value("px")), + false); + } + } + + Inkscape::XML::Node *use = _doc->createElement("svg:use"); + use->setAttribute("xlink:href", id ); + // Set a default style in rather than so it can be changed. + use->setAttribute("style", style ); + if (!Geom::are_near(scale_units, 1.0, Geom::EPSILON)) { + gchar *transform_str = sp_svg_transform_write(Geom::Scale(1.0/scale_units)); + use->setAttribute("transform", transform_str); + g_free(transform_str); + } + _root->appendChild(use); + + // This min and max sets offsets, we don't have any so set to zero. + sp_repr_set_point(_clipnode, "min", Geom::Point(0,0)); + sp_repr_set_point(_clipnode, "max", Geom::Point(0,0)); + + fit_canvas_to_drawing(_clipboardSPDoc); + _setClipboardTargets(); +} + +/** + * Paste from the system clipboard into the active desktop. + * @param in_place Whether to put the contents where they were when copied. + */ +bool ClipboardManagerImpl::paste(SPDesktop *desktop, bool in_place) +{ + // do any checking whether we really are able to paste before requesting the contents + if ( desktop == nullptr ) { + return false; + } + if ( Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false ) { + return false; + } + + Glib::ustring target = _getBestTarget(); + + // Special cases of clipboard content handling go here + // Note that target priority is determined in _getBestTarget. + // TODO: Handle x-special/gnome-copied-files and text/uri-list to support pasting files + + // if there is an image on the clipboard, paste it + if ( target == CLIPBOARD_GDK_PIXBUF_TARGET ) { + return _pasteImage(desktop->doc()); + } + // if there's only text, paste it into a selected text object or create a new one + if ( target == CLIPBOARD_TEXT_TARGET ) { + return _pasteText(desktop); + } + + // otherwise, use the import extensions + SPDocument *tempdoc = _retrieveClipboard(target); + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return false; + } + + sp_import_document(desktop, tempdoc, in_place); + tempdoc->doUnref(); + + // _copySelection() has put all items in groups, now ungroup them (preserves transform + // relationships of clones, text-on-path, etc.) + desktop->selection->ungroup(); + + return true; +} + +/** + * Returns the id of the first visible copied object. + */ +const gchar *ClipboardManagerImpl::getFirstObjectID() +{ + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + return nullptr; + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + if (!root) { + return nullptr; + } + + Inkscape::XML::Node *ch = root->firstChild(); + while (ch != nullptr && + strcmp(ch->name(), "svg:g") && + strcmp(ch->name(), "svg:path") && + strcmp(ch->name(), "svg:use") && + strcmp(ch->name(), "svg:text") && + strcmp(ch->name(), "svg:image") && + strcmp(ch->name(), "svg:rect") && + strcmp(ch->name(), "svg:ellipse") + ) { + ch = ch->next(); + } + + if (ch) { + return ch->attribute("id"); + } + + return nullptr; +} + + +/** + * Implements the Paste Style action. + */ +bool ClipboardManagerImpl::pasteStyle(ObjectSet *set) +{ + if (set->desktop() == nullptr) { + return false; + } + + // check whether something is selected + if (set->isEmpty()) { + _userWarn(set->desktop(), _("Select object(s) to paste style to.")); + return false; + } + + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + // no document, but we can try _text_style + if (_text_style) { + sp_desktop_set_style(set, set->desktop(), _text_style); + return true; + } else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + return false; + } + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + + bool pasted = false; + + if (clipnode) { + set->document()->importDefs(tempdoc); + SPCSSAttr *style = sp_repr_css_attr(clipnode, "style"); + sp_desktop_set_style(set, set->desktop(), style); + pasted = true; + } + else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + } + + tempdoc->doUnref(); + return pasted; +} + + +/** + * Resize the selection or each object in the selection to match the clipboard's size. + * @param separately Whether to scale each object in the selection separately + * @param apply_x Whether to scale the width of objects / selection + * @param apply_y Whether to scale the height of objects / selection + */ +bool ClipboardManagerImpl::pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) +{ + if (!apply_x && !apply_y) { + return false; // pointless parameters + } + +/* if ( desktop == NULL ) { + return false; + } + Inkscape::Selection *selection = desktop->getSelection();*/ + if (set->isEmpty()) { + if(set->desktop()) + _userWarn(set->desktop(), _("Select object(s) to paste size to.")); + return false; + } + + // FIXME: actually, this should accept arbitrary documents + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + if(set->desktop()) + _userWarn(set->desktop(), _("No size on the clipboard.")); + return false; + } + + // retrieve size information from the clipboard + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + bool pasted = false; + if (clipnode) { + Geom::Point min, max; + sp_repr_get_point(clipnode, "min", &min); + sp_repr_get_point(clipnode, "max", &max); + + // resize each object in the selection + if (separately) { + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (item) { + Geom::OptRect obj_size = item->desktopVisualBounds(); + if ( obj_size ) { + item->scale_rel(_getScale(set->desktop(), min, max, *obj_size, apply_x, apply_y)); + } + } else { + g_assert_not_reached(); + } + } + } + // resize the selection as a whole + else { + Geom::OptRect sel_size = set->visualBounds(); + if ( sel_size ) { + set->setScaleRelative(sel_size->midpoint(), + _getScale(set->desktop(), min, max, *sel_size, apply_x, apply_y)); + } + } + pasted = true; + } + tempdoc->doUnref(); + return pasted; +} + + +/** + * Applies a path effect from the clipboard to the selected path. + */ +bool ClipboardManagerImpl::pastePathEffect(ObjectSet *set) +{ + /** @todo FIXME: pastePathEffect crashes when moving the path with the applied effect, + segfaulting in fork_private_if_necessary(). */ + + if ( set->desktop() == nullptr ) { + return false; + } + + //Inkscape::Selection *selection = desktop->getSelection(); + if (!set || set->isEmpty()) { + _userWarn(set->desktop(), _("Select object(s) to paste live path effect to.")); + return false; + } + + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc ) { + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + if ( clipnode ) { + gchar const *effectstack = clipnode->attribute("inkscape:path-effect"); + if ( effectstack ) { + set->document()->importDefs(tempdoc); + // make sure all selected items are converted to paths first (i.e. rectangles) + set->toLPEItems(); + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + _applyPathEffect(item, effectstack); + } + + return true; + } + } + } + + // no_effect: + _userWarn(set->desktop(), _("No effect on the clipboard.")); + return false; +} + + +/** + * Get LPE path data from the clipboard. + * @return The retrieved path data (contents of the d attribute), or "" if no path was found + */ +Glib::ustring ClipboardManagerImpl::getPathParameter(SPDesktop* desktop) +{ + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *path = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth + if ( path == nullptr ) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + tempdoc->doUnref(); + return ""; + } + gchar const *svgd = path->attribute("d"); + return svgd; +} + + +/** + * Get object id of a shape or text item from the clipboard. + * @return The retrieved id string (contents of the id attribute), or "" if no shape or text item was found. + */ +Glib::ustring ClipboardManagerImpl::getShapeOrTextObjectId(SPDesktop *desktop) +{ + // https://bugs.launchpad.net/inkscape/+bug/1293979 + // basically, when we do a depth-first search, we're stopping + // at the first object to be or . + // but that could then return the id of the object's + // clip path or mask, not the original path! + + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + + Inkscape::XML::Node *repr = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth + if ( repr == nullptr ) { + repr = sp_repr_lookup_name(root, "svg:text", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:ellipse", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:rect", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:circle", -1); + } + + + if ( repr == nullptr ) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + tempdoc->doUnref(); + return ""; + } + gchar const *svgd = repr->attribute("id"); + return svgd; +} + +/** + * Get all objects id from the clipboard. + * @return A vector containing all IDs or empty if no shape or text item was found. + * type. Set to "*" to retrieve all elements of the types vector inside, feel free to populate more + */ +std::vector ClipboardManagerImpl::getElementsOfType(SPDesktop *desktop, gchar const* type, gint maxdepth) +{ + std::vector result; + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return result; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + std::vector reprs; + if (strcmp(type, "*") == 0){ + //TODO:Fill vector with all possible elements + std::vector types; + types.push_back((Glib::ustring)"svg:path"); + types.push_back((Glib::ustring)"svg:circle"); + types.push_back((Glib::ustring)"svg:rect"); + types.push_back((Glib::ustring)"svg:ellipse"); + types.push_back((Glib::ustring)"svg:text"); + types.push_back((Glib::ustring)"svg:use"); + types.push_back((Glib::ustring)"svg:g"); + types.push_back((Glib::ustring)"svg:image"); + for (auto type_elem : types) { + std::vector reprs_found = sp_repr_lookup_name_many(root, type_elem.c_str(), maxdepth); // unlimited search depth + reprs.insert(reprs.end(), reprs_found.begin(), reprs_found.end()); + } + } else { + reprs = sp_repr_lookup_name_many(root, type, maxdepth); + } + for (auto node : reprs) { + result.emplace_back(node->attribute("id")); + } + if ( result.empty() ) { + _userWarn(desktop, ((Glib::ustring)_("Clipboard does not contain any.") + (Glib::ustring)type).c_str()); + tempdoc->doUnref(); + return result; + } + return result; +} + +/** + * Iterate over a list of items and copy them to the clipboard. + */ +void ClipboardManagerImpl::_copySelection(ObjectSet *selection) +{ + // copy the defs used by all items + auto itemlist= selection->items(); + cloned_elements.clear(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (item) { + _copyUsedDefs(item); + } else { + g_assert_not_reached(); + } + } + + // copy the representation of the items + std::vector sorted_items(itemlist.begin(), itemlist.end()); + { + // Get external text references and add them to sorted_items + auto ext_refs = text_categorize_refs(selection->document(), + sorted_items.begin(), sorted_items.end(), + TEXT_REF_EXTERNAL); + for (auto const &ext_ref : ext_refs) { + sorted_items.push_back(selection->document()->getObjectById(ext_ref.first)); + } + } + sort(sorted_items.begin(), sorted_items.end(), sp_object_compare_position_bool); + + //remove already copied elements from cloned_elements + std::vectortr; + for(auto cloned_element : cloned_elements){ + if(std::find(sorted_items.begin(),sorted_items.end(),cloned_element)!=sorted_items.end()) + tr.push_back(cloned_element); + } + for(auto & it : tr){ + cloned_elements.erase(it); + } + + // One group per shared parent + std::map groups; + + sorted_items.insert(sorted_items.end(),cloned_elements.begin(),cloned_elements.end()); + for(auto sorted_item : sorted_items){ + SPItem *item = dynamic_cast(sorted_item); + if (item) { + // Create a group with the parent transform. This group will be ungrouped when pasting + // und takes care of transform relationships of clones, text-on-path, etc. + auto &group = groups[item->parent]; + if (!group) { + group = _doc->createElement("svg:g"); + _root->appendChild(group); + Inkscape::GC::release(group); + + if (auto parent = dynamic_cast(item->parent)) { + auto transform_str = sp_svg_transform_write(parent->i2doc_affine()); + group->setAttributeOrRemoveIfEmpty("transform", transform_str); + g_free(transform_str); + } + } + + Inkscape::XML::Node *obj = item->getRepr(); + Inkscape::XML::Node *obj_copy; + if(cloned_elements.find(item)==cloned_elements.end()) + obj_copy = _copyNode(obj, _doc, group); + else + obj_copy = _copyNode(obj, _doc, _clipnode); + + // copy complete inherited style + SPCSSAttr *css = sp_repr_css_attr_inherited(obj, "style"); + for (auto iter : item->style->properties()) { + if (iter->style_src == SP_STYLE_SRC_STYLE_SHEET) { + css->setAttributeOrRemoveIfEmpty(iter->name(), iter->get_value()); + } + } + sp_repr_css_set(obj_copy, css, "style"); + sp_repr_css_attr_unref(css); + } + } + + // copy style for Paste Style action + if (!sorted_items.empty()) { + SPObject *object = sorted_items[0]; + SPItem *item = dynamic_cast(object); + if (item) { + SPCSSAttr *style = take_style_from_item(item); + sp_repr_css_set(_clipnode, style, "style"); + sp_repr_css_attr_unref(style); + } + // copy path effect from the first path + if (object) { + gchar const *effect =object->getRepr()->attribute("inkscape:path-effect"); + if (effect) { + _clipnode->setAttribute("inkscape:path-effect", effect); + } + } + } + + Geom::OptRect size = selection->visualBounds(); + if (size) { + sp_repr_set_point(_clipnode, "min", size->min()); + sp_repr_set_point(_clipnode, "max", size->max()); + } + +} + + +/** + * Recursively copy all the definitions used by a given item to the clipboard defs. + */ +void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) +{ + SPUse *use=dynamic_cast(item); + if (use && use->get_original()) { + if(cloned_elements.insert(use->get_original()).second) + _copyUsedDefs(use->get_original()); + } + + // copy fill and stroke styles (patterns and gradients) + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( dynamic_cast(server) || dynamic_cast(server) || dynamic_cast(server) ) { + _copyGradient(dynamic_cast(server)); + } + SPPattern *pattern = dynamic_cast(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast(server); + if (hatch) { + _copyHatch(hatch); + } + } + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( dynamic_cast(server) || dynamic_cast(server) || dynamic_cast(server) ) { + _copyGradient(dynamic_cast(server)); + } + SPPattern *pattern = dynamic_cast(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast(server); + if (hatch) { + _copyHatch(hatch); + } + } + + // For shapes, copy all of the shape's markers + SPShape *shape = dynamic_cast(item); + if (shape) { + for (auto & i : shape->_marker) { + if (i) { + _copyNode(i->getRepr(), _doc, _defs); + } + } + } + + // For 3D boxes, copy perspectives + { + SPBox3D *box = dynamic_cast(item); + if (box) { + _copyNode(box3d_get_perspective(box)->getRepr(), _doc, _defs); + } + } + + // Copy text elements + { + SPText *text = dynamic_cast(item); + if (text) { + // Copy text paths + SPTextPath *textpath = dynamic_cast(text->firstChild()); + if (textpath) { + _copyTextPath(textpath); + } + // Copy text shape-inside + Inkscape::XML::Node* rectangle = text->get_first_rectangle(); + if (rectangle) { + _copyNode(rectangle, _doc, _defs); + } + } + if (text) { + for (auto &&shape_prop_ptr : { + reinterpret_cast(&SPStyle::shape_inside), + reinterpret_cast(&SPStyle::shape_subtract) }) { + for (auto const &shape_id : (text->style->*shape_prop_ptr).shape_ids) { + auto shape_repr = text->document->getObjectById(shape_id)->getRepr(); + if (sp_repr_is_def(shape_repr)) { + _copyIgnoreDup(shape_repr, _doc, _defs); + } + } + } + } + } + + + // Copy clipping objects + if (SPObject *clip = item->getClipObject()) { + _copyNode(clip->getRepr(), _doc, _defs); + } + // Copy mask objects + if (SPObject *mask = item->getMaskObject()) { + _copyNode(mask->getRepr(), _doc, _defs); + // recurse into the mask for its gradients etc. + for(auto& o: mask->children) { + SPItem *childItem = dynamic_cast(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } + } + + // Copy filters + if (style->getFilter()) { + SPObject *filter = style->getFilter(); + if (dynamic_cast(filter)) { + _copyNode(filter->getRepr(), _doc, _defs); + } + } + + // For lpe items, copy lpe stack if applicable + SPLPEItem *lpeitem = dynamic_cast(item); + if (lpeitem) { + if (lpeitem->hasPathEffect()) { + PathEffectList path_effect_list( *lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + _copyNode(lpeobj->getRepr(), _doc, _defs); + } + } + } + } + + // recurse + for(auto& o: item->children) { + SPItem *childItem = dynamic_cast(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } +} + +/** + * Copy a single gradient to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyGradient(SPGradient *gradient) +{ + while (gradient) { + // climb up the refs, copying each one in the chain + _copyNode(gradient->getRepr(), _doc, _defs); + if (gradient->ref){ + gradient = gradient->ref->getObject(); + } + else { + gradient = nullptr; + } + } +} + + +/** + * Copy a single pattern to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyPattern(SPPattern *pattern) +{ + // climb up the references, copying each one in the chain + while (pattern) { + _copyNode(pattern->getRepr(), _doc, _defs); + + // items in the pattern may also use gradients and other patterns, so recurse + for (auto& child: pattern->children) { + SPItem *childItem = dynamic_cast(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (pattern->ref){ + pattern = pattern->ref->getObject(); + } + else{ + pattern = nullptr; + } + } +} + +/** + * Copy a single hatch to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyHatch(SPHatch *hatch) +{ + // climb up the references, copying each one in the chain + while (hatch) { + _copyNode(hatch->getRepr(), _doc, _defs); + + for (auto &child : hatch->children) { + SPItem *childItem = dynamic_cast(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (hatch->ref) { + hatch = hatch->ref->getObject(); + } else { + hatch = nullptr; + } + } +} + + +/** + * Copy a text path to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyTextPath(SPTextPath *tp) +{ + SPItem *path = sp_textpath_get_path_item(tp); + if (!path) { + return; + } + // textpaths that aren't in defs (on the canvas) shouldn't be copied because if + // both objects are being copied already, this ends up stealing the refs id. + if(path->parent && SP_IS_DEFS(path->parent)) { + _copyIgnoreDup(path->getRepr(), _doc, _defs); + } +} + + +/** + * Copy a single XML node from one document to another. + * @param node The node to be copied + * @param target_doc The document to which the node is to be copied + * @param parent The node in the target document which will become the parent of the copied node + * @return Pointer to the copied node + */ +Inkscape::XML::Node *ClipboardManagerImpl::_copyNode(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + +Inkscape::XML::Node *ClipboardManagerImpl::_copyIgnoreDup(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + if (sp_repr_lookup_child(_root, "id", node->attribute("id"))) { + // node already copied + return nullptr; + } + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + + +/** + * Retrieve a bitmap image from the clipboard and paste it into the active document. + */ +bool ClipboardManagerImpl::_pasteImage(SPDocument *doc) +{ + if ( doc == nullptr ) { + return false; + } + + // retrieve image data + Glib::RefPtr img = _clipboard->wait_for_image(); +#ifdef _WIN32 + // For some reason the first call to wait_for_image() often fails, despite image data being available + // TODO: Figure out why that is and remove this hack. + if (!img) { + img = _clipboard->wait_for_image(); + } +#endif + if (!img) { + return false; + } + + Inkscape::Extension::Extension *png = Inkscape::Extension::find_by_mime("image/png"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring attr_saved = prefs->getString("/dialogs/import/link"); + bool ask_saved = prefs->getBool("/dialogs/import/ask"); + prefs->setString("/dialogs/import/link", "embed"); + prefs->setBool("/dialogs/import/ask", false); + png->set_gui(false); + + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", NULL ); + img->save(filename, "png"); + file_import(doc, filename, png); + g_free(filename); + prefs->setString("/dialogs/import/link", attr_saved); + prefs->setBool("/dialogs/import/ask", ask_saved); + png->set_gui(true); + + return true; +} + +/** + * Paste text into the selected text object or create a new one to hold it. + */ +bool ClipboardManagerImpl::_pasteText(SPDesktop *desktop) +{ + if ( desktop == nullptr ) { + return false; + } + + // if the text editing tool is active, paste the text into the active text object + if (tools_isactive(desktop, TOOLS_TEXT)) { + return Inkscape::UI::Tools::sp_text_paste_inline(desktop->event_context); + } + return false; + /* return false; + //apply the saved style to pasted text + Glib::RefPtr refClipboard = Gtk::Clipboard::get(); + Glib::ustring const clip_text = refClipboard->wait_for_text(); + Glib::ustring text(clip_text); + if(text.length() == copied_style_length) + { + Inkscape::UI::Tools::TextTool *tc = SP_TEXT_CONTEXT(desktop->event_context); + // we realy only want to inherit container style (to act as 0.92 and faster performance) + // maybe for 1.0 we can make a special type of clipboard + // that handle layout or maybe we can use the last desktop text style + // so I comment unneded code. + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + Inkscape::Text::Layout::iterator it_next; + Inkscape::Text::Layout::iterator it = tc->text_sel_end; + SPText *textitem = dynamic_cast(tc->text); + if (textitem) { + textitem->rebuildLayout(); + } + SPFlowtext *flowtext = dynamic_cast(tc->text); + if (flowtext) { + flowtext->rebuildLayout(); + } + // we realy only want to inherit container style + SPCSSAttr *css = take_style_from_item(tc->text); + for (int i = 0; i < nr_blocks; ++i) + { + gchar const *w = sp_repr_css_property(css, "font-size", "0px"); + + // Don't set font-size if it wasn't set. + if (w && strcmp(w, "0px") != 0) { + sp_repr_css_set_property(te_selected_style[i], "font-size", w); + } + } + + for (unsigned int i = 0; i < text.length(); ++i) + it.prevCharacter(); + + it_next = layout->charIndexToIterator(layout->iteratorToCharIndex(it)); + + for (int i = 0; i < nr_blocks; ++i) + { + for (unsigned int j = te_selected_style_positions[i]; j < te_selected_style_positions[i+1]; ++j) + it_next.nextCharacter(); + + // sp_te_apply_style(tc->text, it, it_next, te_selected_style[i]); + te_update_layout_now_recursive(tc->text); + tc->text_sel_end = it; + for (unsigned int j = te_selected_style_positions[i]; j < te_selected_style_positions[i+1]; ++j) + it.nextCharacter(); + } + } + return true; + + } + // old(try to parse the text as a color and, if successful, apply it as the current style) + // we realy only want to inherit container style + // maybe for 1.0 we can make a special type of clipboard + // that handle layout or maybe we can use the last desktop text style + SPCSSAttr *css = sp_repr_css_attr_parse_color_to_fill(_clipboard->wait_for_text()); + if (css) { + sp_desktop_set_style(desktop, css); + return true; + } + return false; + */ +} + + +/** + * Applies a pasted path effect to a given item. + */ +void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effectstack) +{ + if ( item == nullptr ) { + return; + } + + SPLPEItem *lpeitem = dynamic_cast(item); + if (lpeitem && effectstack) { + std::istringstream iss(effectstack); + std::string href; + while (std::getline(iss, href, ';')) + { + SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc, href.c_str()); + if (!obj) { + return; + } + LivePathEffectObject *lpeobj = dynamic_cast(obj); + if (lpeobj) { + lpeitem->addPathEffect(lpeobj); + } + } + // for each effect in the stack, check if we need to fork it before adding it to the item + lpeitem->forkPathEffectsIfNecessary(1); + } +} + + +/** + * Retrieve the clipboard contents as a document. + * @return Clipboard contents converted to SPDocument, or NULL if no suitable content was present + */ +SPDocument *ClipboardManagerImpl::_retrieveClipboard(Glib::ustring required_target) +{ + Glib::ustring best_target; + if ( required_target == "" ) { + best_target = _getBestTarget(); + } else { + best_target = required_target; + } + + if ( best_target == "" ) { + return nullptr; + } + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", NULL ); + + bool file_saved = false; + Glib::ustring target = best_target; + +#ifdef _WIN32 + if (best_target == "CF_ENHMETAFILE" || best_target == "WCF_ENHMETAFILE") + { // Try to save clipboard data as en emf file (using win32 api) + if (OpenClipboard(NULL)) { + HGLOBAL hglb = GetClipboardData(CF_ENHMETAFILE); + if (hglb) { + HENHMETAFILE hemf = CopyEnhMetaFile((HENHMETAFILE) hglb, filename); + if (hemf) { + file_saved = true; + target = "image/x-emf"; + DeleteEnhMetaFile(hemf); + } + } + CloseClipboard(); + } + } +#endif + + if (!file_saved) { + if ( !_clipboard->wait_is_target_available(best_target) ) { + return nullptr; + } + + // doing this synchronously makes better sense + // TODO: use another method because this one is badly broken imo. + // from documentation: "Returns: A SelectionData object, which will be invalid if retrieving the given target failed." + // I don't know how to check whether an object is 'valid' or not, unusable if that's not possible... + Gtk::SelectionData sel = _clipboard->wait_for_contents(best_target); + target = sel.get_target(); // this can crash if the result was invalid of last function. No way to check for this :( + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + g_file_set_contents(filename, (const gchar *) sel.get_data(), sel.get_length(), nullptr); + } + + // there is no specific plain SVG input extension, so if we can paste the Inkscape SVG format, + // we use the image/svg+xml mimetype to look up the input extension + if (target == "image/x-inkscape-svg") { + target = "image/svg+xml"; + } + // Use the EMF extension to import metafiles + if (target == "CF_ENHMETAFILE" || target == "WCF_ENHMETAFILE") { + target = "image/x-emf"; + } + + Inkscape::Extension::DB::InputList inlist; + Inkscape::Extension::db.get_input_list(inlist); + Inkscape::Extension::DB::InputList::const_iterator in = inlist.begin(); + for (; in != inlist.end() && target != (*in)->get_mimetype() ; ++in) { + }; + if ( in == inlist.end() ) { + return nullptr; // this shouldn't happen unless _getBestTarget returns something bogus + } + + SPDocument *tempdoc = nullptr; + try { + tempdoc = (*in)->open(filename); + tempdoc->doRef(); + } catch (...) { + } + g_unlink(filename); + g_free(filename); + + return tempdoc; +} + + +/** + * Callback called when some other application requests data from Inkscape. + * + * Finds a suitable output extension to save the internal clipboard document, + * then saves it to memory and sets the clipboard contents. + */ +void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) +{ + if (_clipboardSPDoc == nullptr) + return; + + Glib::ustring target = sel.get_target(); + if (target == "") { + return; // this shouldn't happen + } + + if (target == CLIPBOARD_TEXT_TARGET) { + target = "image/x-inkscape-svg"; + } + + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + }; + if ( out == outlist.end() && target != "image/png") { + // This happens when hitting "optpng" extensions + return; + } + + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export", NULL ); + gchar *data = nullptr; + gsize len; + + // XXX This is a crude fix for clipboards accessing extensions + // Remove when gui is extracted from extension execute and uses exceptions. + bool previous_gui = INKSCAPE.use_gui(); + INKSCAPE.use_gui(false); + + try { + if (out == outlist.end() && target == "image/png") + { + gdouble dpi = Inkscape::Util::Quantity::convert(1, "in", "px"); + guint32 bgcolor = 0x00000000; + + Geom::Point origin (_clipboardSPDoc->getRoot()->x.computed, _clipboardSPDoc->getRoot()->y.computed); + Geom::Rect area = Geom::Rect(origin, origin + _clipboardSPDoc->getDimensions()); + + unsigned long int width = (unsigned long int) (area.width() + 0.5); + unsigned long int height = (unsigned long int) (area.height() + 0.5); + + // read from namedview + Inkscape::XML::Node *nv = _clipboardSPDoc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")) { + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")) { + double opacity = 1.0; + sp_repr_get_double(nv, "inkscape:pageopacity", &opacity); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + std::vector x; + sp_export_png_file(_clipboardSPDoc, filename, area, width, height, dpi, dpi, bgcolor, nullptr, nullptr, true, x); + } + else + { + if (!(*out)->loaded()) { + // Need to load the extension. + (*out)->set_state(Inkscape::Extension::Extension::STATE_LOADED); + } + + (*out)->save(_clipboardSPDoc, filename, true); + } + g_file_get_contents(filename, &data, &len, nullptr); + + sel.set(8, (guint8 const *) data, len); + } catch (...) { + } + + INKSCAPE.use_gui(previous_gui); + g_unlink(filename); // delete the temporary file + g_free(filename); + g_free(data); +} + + +/** + * Callback when someone else takes the clipboard. + * + * When the clipboard owner changes, this callback clears the internal clipboard document + * to reduce memory usage. + */ +void ClipboardManagerImpl::_onClear() +{ + // why is this called before _onGet??? + //_discardInternalClipboard(); +} + + +/** + * Creates an internal clipboard document from scratch. + */ +void ClipboardManagerImpl::_createInternalClipboard() +{ + if ( _clipboardSPDoc == nullptr ) { + _clipboardSPDoc = SPDocument::createNewDoc(nullptr, false, true); + //g_assert( _clipboardSPDoc != NULL ); + _defs = _clipboardSPDoc->getDefs()->getRepr(); + _doc = _clipboardSPDoc->getReprDoc(); + _root = _clipboardSPDoc->getReprRoot(); + + if (SP_ACTIVE_DOCUMENT) { + _clipboardSPDoc->setDocumentBase(SP_ACTIVE_DOCUMENT->getDocumentBase()); + } + + _clipnode = _doc->createElement("inkscape:clipboard"); + _root->appendChild(_clipnode); + Inkscape::GC::release(_clipnode); + + // once we create a SVG document, style will be stored in it, so flush _text_style + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + } +} + + +/** + * Deletes the internal clipboard document. + */ +void ClipboardManagerImpl::_discardInternalClipboard() +{ + if ( _clipboardSPDoc != nullptr ) { + _clipboardSPDoc->doUnref(); + _clipboardSPDoc = nullptr; + _defs = nullptr; + _doc = nullptr; + _root = nullptr; + _clipnode = nullptr; + } +} + + +/** + * Get the scale to resize an item, based on the command and desktop state. + */ +Geom::Scale ClipboardManagerImpl::_getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y) +{ + double scale_x = 1.0; + double scale_y = 1.0; + + if (apply_x) { + scale_x = (max[Geom::X] - min[Geom::X]) / obj_rect[Geom::X].extent(); + } + if (apply_y) { + scale_y = (max[Geom::Y] - min[Geom::Y]) / obj_rect[Geom::Y].extent(); + } + // If the "lock aspect ratio" button is pressed and we paste only a single coordinate, + // resize the second one by the same ratio too + if (desktop && desktop->isToolboxButtonActive("lock")) { + if (apply_x && !apply_y) { + scale_y = scale_x; + } + if (apply_y && !apply_x) { + scale_x = scale_y; + } + } + + return Geom::Scale(scale_x, scale_y); +} + + +/** + * Find the most suitable clipboard target. + */ +Glib::ustring ClipboardManagerImpl::_getBestTarget() +{ + auto targets = _clipboard->wait_for_targets(); + + // clipboard target debugging snippet + /* + g_message("Begin clipboard targets"); + for ( std::list::iterator x = targets.begin() ; x != targets.end(); ++x ) + g_message("Clipboard target: %s", (*x).data()); + g_message("End clipboard targets\n"); + //*/ + + for (auto & _preferred_target : _preferred_targets) + { + if ( std::find(targets.begin(), targets.end(), _preferred_target) != targets.end() ) { + return _preferred_target; + } + } +#ifdef _WIN32 + if (OpenClipboard(NULL)) + { // If both bitmap and metafile are present, pick the one that was exported first. + UINT format = EnumClipboardFormats(0); + while (format) { + if (format == CF_ENHMETAFILE || format == CF_DIB || format == CF_BITMAP) { + break; + } + format = EnumClipboardFormats(format); + } + CloseClipboard(); + + if (format == CF_ENHMETAFILE) { + return "CF_ENHMETAFILE"; + } + if (format == CF_DIB || format == CF_BITMAP) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + } + + if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) { + return "CF_ENHMETAFILE"; + } +#endif + if (_clipboard->wait_is_image_available()) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + if (_clipboard->wait_is_text_available()) { + return CLIPBOARD_TEXT_TARGET; + } + + return ""; +} + + +/** + * Set the clipboard targets to reflect the mimetypes Inkscape can output. + */ +void ClipboardManagerImpl::_setClipboardTargets() +{ + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + std::vector target_list; + + bool plaintextSet = false; + for (Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin() ; out != outlist.end() ; ++out) { + if ( !(*out)->deactivated() ) { + Glib::ustring mime = (*out)->get_mimetype(); + if (mime != CLIPBOARD_TEXT_TARGET) { + if ( !plaintextSet && (mime.find("svg") == Glib::ustring::npos) ) { + target_list.emplace_back(CLIPBOARD_TEXT_TARGET); + plaintextSet = true; + } + target_list.emplace_back(mime); + } + } + } + + // Add PNG export explicitly since there is no extension for this... + // On Windows, GTK will also present this as a CF_DIB/CF_BITMAP + target_list.emplace_back( "image/png" ); + + _clipboard->set(target_list, + sigc::mem_fun(*this, &ClipboardManagerImpl::_onGet), + sigc::mem_fun(*this, &ClipboardManagerImpl::_onClear)); + +#ifdef _WIN32 + // If the "image/x-emf" target handled by the emf extension would be + // presented as a CF_ENHMETAFILE automatically (just like an "image/bmp" + // is presented as a CF_BITMAP) this code would not be needed.. ??? + // Or maybe there is some other way to achieve the same? + + // Note: Metafile is the only format that is rendered and stored in clipboard + // on Copy, all other formats are rendered only when needed by a Paste command. + + // FIXME: This should at least be rewritten to use "delayed rendering". + // If possible make it delayed rendering by using GTK API only. + + if (OpenClipboard(NULL)) { + if ( _clipboardSPDoc != NULL ) { + const Glib::ustring target = "image/x-emf"; + + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + } + if ( out != outlist.end() ) { + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export.emf", NULL ); + + try { + (*out)->save(_clipboardSPDoc, filename); + HENHMETAFILE hemf = GetEnhMetaFileA(filename); + if (hemf) { + SetClipboardData(CF_ENHMETAFILE, hemf); + DeleteEnhMetaFile(hemf); + } + } catch (...) { + } + g_unlink(filename); // delete the temporary file + g_free(filename); + } + } + CloseClipboard(); + } +#endif +} + + +/** + * Set the string representation of a 32-bit RGBA color as the clipboard contents. + */ +void ClipboardManagerImpl::_setClipboardColor(guint32 color) +{ + gchar colorstr[16]; + g_snprintf(colorstr, 16, "%08x", color); + _clipboard->set_text(colorstr); +} + + +/** + * Put a notification on the message stack. + */ +void ClipboardManagerImpl::_userWarn(SPDesktop *desktop, char const *msg) +{ + if(desktop) + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, msg); +} + +/* ####################################### + ClipboardManager class + ####################################### */ + +ClipboardManager *ClipboardManager::_instance = nullptr; + +ClipboardManager::ClipboardManager() = default; +ClipboardManager::~ClipboardManager() = default; +ClipboardManager *ClipboardManager::get() +{ + if ( _instance == nullptr ) { + _instance = new ClipboardManagerImpl; + } + + return _instance; +} + +} // namespace Inkscape +} // namespace IO + +/* + 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 : diff --git a/src/ui/clipboard.h b/src/ui/clipboard.h new file mode 100644 index 0000000..aa96a21 --- /dev/null +++ b/src/ui/clipboard.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief System-wide clipboard management - class declaration + *//* + * Authors: see git history + * Krzysztof Kosiński + * Jon A. Cruz + * + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_CLIPBOARD_H +#define SEEN_INKSCAPE_CLIPBOARD_H + +#include + +// forward declarations +class SPDesktop; +namespace Inkscape { +class ObjectSet; +namespace XML { class Node; } +namespace LivePathEffect { class PathParam; } + +namespace UI { + +/** + * @brief System-wide clipboard manager + * + * ClipboardManager takes care of manipulating the system clipboard in response + * to user actions. It holds a complete SPDocument as the contents. This document + * is exported using output extensions when other applications request data. + * Copying to another instance of Inkscape is special-cased, because of the extra + * data required (i.e. style, size, Live Path Effects parameters, etc.) + */ + +class ClipboardManager { +public: + virtual void copy(ObjectSet *set) = 0; + virtual void copyPathParameter(Inkscape::LivePathEffect::PathParam *) = 0; + virtual void copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol = true) = 0; + virtual bool paste(SPDesktop *desktop, bool in_place = false) = 0; + virtual bool pasteStyle(ObjectSet *set) = 0; + virtual bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) = 0; + virtual bool pastePathEffect(ObjectSet *set) = 0; + virtual Glib::ustring getPathParameter(SPDesktop* desktop) = 0; + virtual Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) = 0; + virtual std::vector getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) = 0; + virtual const gchar *getFirstObjectID() = 0; + static ClipboardManager *get(); +protected: + ClipboardManager(); // singleton + virtual ~ClipboardManager(); +private: + ClipboardManager(const ClipboardManager &) = delete; ///< no copy + ClipboardManager &operator=(const ClipboardManager &) = delete; ///< no assign + + static ClipboardManager *_instance; +}; + +} // namespace IO +} // namespace Inkscape + +#endif +/* + 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 : diff --git a/src/ui/contextmenu.cpp b/src/ui/contextmenu.cpp new file mode 100644 index 0000000..e8851e6 --- /dev/null +++ b/src/ui/contextmenu.cpp @@ -0,0 +1,1008 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Context menu + */ +/* Authors: + * Lauris Kaplinski + * Frank Felfe + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2010 authors + * Copyright (C) 1999-2005 authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "contextmenu.h" + +#include +#include + +#include +#include +#include +#include + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "shortcuts.h" + +#include "helper/action-context.h" +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "live_effects/lpe-powerclip.h" +#include "live_effects/lpe-powermask.h" + +#include "object/sp-anchor.h" +#include "object/sp-clippath.h" +#include "object/sp-image.h" +#include "object/sp-mask.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/layer-properties.h" +#include "verbs.h" + +using Inkscape::DocumentUndo; + +static bool temporarily_block_actions = false; + +ContextMenu::ContextMenu(SPDesktop *desktop, SPItem *item) : + _item(item), + MIGroup(), + MIParent(_("Go to parent")) +{ + _object = static_cast(item); + _desktop = desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_icons = prefs->getInt("/theme/menuIcons_canvas", true); + + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_UNDO), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_REDO), show_icons); + AddSeparator(); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_CUT), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_COPY), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_PASTE), show_icons); + AddSeparator(); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_DUPLICATE), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_DELETE), show_icons); + + positionOfLastDialog = 10; // 9 in front + 1 for the separator in the next if; used to position the dialog menu entries below each other + /* Item menu */ + if (item!=nullptr) { + AddSeparator(); + MakeObjectMenu(); + } + AddSeparator(); + /* Lock/Unock Hide/Unhide*/ + auto point_doc = _desktop->point() * _desktop->dt2doc(); + Geom::Rect b(point_doc, point_doc + Geom::Point(1, 1)); + std::vector< SPItem * > down_items = _desktop->getDocument()->getItemsPartiallyInBox( _desktop->dkey, b, true, true, true, true); + bool has_down_hidden = false; + bool has_down_locked = false; + for(auto & down_item : down_items){ + if(down_item->isHidden()) { + has_down_hidden = true; + } + if(down_item->isLocked()) { + has_down_locked = true; + } + } + Gtk::MenuItem* mi; + + mi = Gtk::manage(new Gtk::MenuItem(_("Hide Selected Objects"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::HideSelected)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Unhide Objects Below"),true)); + mi->signal_activate().connect(sigc::bind >(sigc::mem_fun(*this, &ContextMenu::UnHideBelow), down_items)); + if (!has_down_hidden) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Lock Selected Objects"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::LockSelected)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Unlock Objects Below"),true)); + mi->signal_activate().connect(sigc::bind >(sigc::mem_fun(*this, &ContextMenu::UnLockBelow), down_items)); + if (!has_down_locked) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + /* layer menu */ + SPGroup *group=nullptr; + if (item) { + if (SP_IS_GROUP(item)) { + group = SP_GROUP(item); + } else if ( item != _desktop->currentRoot() && SP_IS_GROUP(item->parent) ) { + group = SP_GROUP(item->parent); + } + } + + if (( group && group != _desktop->currentLayer() ) || + ( _desktop->currentLayer() != _desktop->currentRoot() && _desktop->currentLayer()->parent != _desktop->currentRoot() ) ) { + AddSeparator(); + } + + if ( group && group != _desktop->currentLayer() ) { + /* TRANSLATORS: #%1 is the id of the group e.g. , not a number. */ + MIGroup.set_label (Glib::ustring::compose(_("Enter group #%1"), group->getId())); + MIGroup.set_data("group", group); + MIGroup.signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ContextMenu::EnterGroup),&MIGroup)); + MIGroup.show(); + append(MIGroup); + } + + if ( _desktop->currentLayer() != _desktop->currentRoot() ) { + if ( _desktop->currentLayer()->parent != _desktop->currentRoot() ) { + MIParent.signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::LeaveGroup)); + MIParent.show(); + append(MIParent); + + /* Pop selection out of group */ + Gtk::MenuItem* miu = Gtk::manage(new Gtk::MenuItem(_("_Pop selection out of group"), true)); + miu->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateUngroupPopSelection)); + miu->show(); + append(*miu); + } + } + + signal_map().connect(sigc::mem_fun(*this, &ContextMenu::ShiftIcons)); +} + +ContextMenu::~ContextMenu(void) += default; + +Gtk::SeparatorMenuItem* ContextMenu::AddSeparator() +{ + Gtk::SeparatorMenuItem* sep = Gtk::manage(new Gtk::SeparatorMenuItem()); + sep->show(); + append(*sep); + return sep; +} + +void ContextMenu::EnterGroup(Gtk::MenuItem* mi) +{ + _desktop->setCurrentLayer(reinterpret_cast(mi->get_data("group"))); + _desktop->selection->clear(); +} + +void ContextMenu::LeaveGroup() +{ + _desktop->setCurrentLayer(_desktop->currentLayer()->parent); +} + +void ContextMenu::LockSelected() +{ + auto itemlist = _desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i) { + (*i)->setLocked(true); + } +} + +void ContextMenu::HideSelected() +{ + auto itemlist =_desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i) { + (*i)->setHidden(true); + } +} + +void ContextMenu::UnLockBelow(std::vector items) +{ + _desktop->selection->clear(); + for(auto & item : items) { + if (item->isLocked()) { + item->setLocked(false); + _desktop->selection->add(item); + } + } +} + +void ContextMenu::UnHideBelow(std::vector items) +{ + _desktop->selection->clear(); + for(auto & item : items) { + if (item->isHidden()) { + item->setHidden(false); + _desktop->selection->add(item); + } + } +} + +/* + * Some day when the right-click menus are ready to start working + * smarter with the verbs, we'll need to change this NULL being + * sent to sp_action_perform to something useful, or set some kind + * of global "right-clicked position" variable for actions to + * investigate when they're called. + */ +static void +context_menu_item_on_my_activate(void */*object*/, SPAction *action) +{ + if (!temporarily_block_actions) { + sp_action_perform(action, nullptr); + } +} + +static void +context_menu_item_on_my_select(void */*object*/, SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, action->tip); +} + +static void +context_menu_item_on_my_deselect(void */*object*/, SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->clear(); +} + + +// TODO: Update this to allow radio items to be used +void ContextMenu::AppendItemFromVerb(Inkscape::Verb *verb, bool show_icon) +{ + SPAction *action; + SPDesktop *view = _desktop; + + if (verb->get_code() == SP_VERB_NONE) { + Gtk::MenuItem *item = AddSeparator(); + item->show(); + append(*item); + } else { + action = verb->get_action(Inkscape::ActionContext(view)); + if (!action) { + return; + } + // Create the menu item itself + auto const item = Gtk::manage(new Gtk::MenuItem()); + + // Now create the label and add it to the menu item (with mnemonic) + auto const label = Gtk::manage(new Gtk::AccelLabel(action->name, true)); + label->set_xalign(0.0); + sp_shortcut_add_accelerator(GTK_WIDGET(item->gobj()), sp_shortcut_get_primary(verb)); + label->set_accel_widget(*item); + + // If there is an image associated with the action, then we can add it as an icon for the menu item + if (show_icon && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + auto const icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // create a box to hold icon and label as GtkMenuItem derives from GtkBin and can only hold one child + auto const box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + + item->add(*box); + } else { + item->add(*label); + } + + action->signal_set_sensitive.connect(sigc::mem_fun(*this, &ContextMenu::set_sensitive)); + action->signal_set_name.connect(sigc::mem_fun(*item, &ContextMenu::set_name)); + + if (!action->sensitive) { + item->set_sensitive(FALSE); + } + + item->set_events(Gdk::KEY_PRESS_MASK); + item->signal_activate().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_activate),item,action)); + item->signal_select().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_select),item,action)); + item->signal_deselect().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_deselect),item,action)); + item->show_all(); + + append(*item); + } +} + +void ContextMenu::MakeObjectMenu() +{ + if (SP_IS_ITEM(_object)) { + MakeItemMenu(); + } + + if (SP_IS_GROUP(_object)) { + MakeGroupMenu(); + } + + if (SP_IS_ANCHOR(_object)) { + MakeAnchorMenu(); + } + + if (SP_IS_IMAGE(_object)) { + MakeImageMenu(); + } + + if (SP_IS_SHAPE(_object)) { + MakeShapeMenu(); + } + + if (SP_IS_TEXT(_object)) { + MakeTextMenu(); + } +} + +void ContextMenu::MakeItemMenu () +{ + Gtk::MenuItem* mi; + + /* Item dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Object Properties..."),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemProperties)); + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + AddSeparator(); + + /* Select item */ + if (Inkscape::Verb::getbyid( "org.inkscape.follow_link" )) { + mi = Gtk::manage(new Gtk::MenuItem(_("_Select This"), true)); + if (_desktop->selection->includes(_item)) { + mi->set_sensitive(FALSE); + } else { + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemSelectThis)); + } + mi->show(); + append(*mi); + } + + + mi = Gtk::manage(new Gtk::MenuItem(_("Select Same"))); + mi->show(); + Gtk::Menu *select_same_submenu = Gtk::manage(new Gtk::Menu()); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } + mi->set_submenu(*select_same_submenu); + append(*mi); + + /* Select same fill and stroke */ + mi = Gtk::manage(new Gtk::MenuItem(_("Fill and Stroke"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameFillStroke)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same fill color */ + mi = Gtk::manage(new Gtk::MenuItem(_("Fill Color"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameFillColor)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke color */ + mi = Gtk::manage(new Gtk::MenuItem(_("Stroke Color"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameStrokeColor)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke style */ + mi = Gtk::manage(new Gtk::MenuItem(_("Stroke Style"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameStrokeStyle)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke style */ + mi = Gtk::manage(new Gtk::MenuItem(_("Object Type"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameObjectType)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Move to layer */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Move to Layer..."), true)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } else { + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemMoveTo)); + } + mi->show(); + append(*mi); + + /* Create link */ + mi = Gtk::manage(new Gtk::MenuItem(_("Create _Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemCreateLink)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + append(*mi); + + bool ClipRefOK=false; + bool MaskRefOK=false; + if (_item && _item->getClipObject()) { + ClipRefOK = true; + } + if (_item && _item->getMaskObject()) { + MaskRefOK = true; + } + /* Set mask */ + mi = Gtk::manage(new Gtk::MenuItem(_("Set Mask"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SetMask)); + if (ClipRefOK || MaskRefOK) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); + + /* Release mask */ + mi = Gtk::manage(new Gtk::MenuItem(_("Release Mask"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ReleaseMask)); + if (MaskRefOK) { + mi->set_sensitive(TRUE); + } else { + mi->set_sensitive(FALSE); + } + mi->show(); + append(*mi); + + /*SSet Clip Group */ + mi = Gtk::manage(new Gtk::MenuItem(_("Create Clip G_roup"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::CreateGroupClip)); + mi->set_sensitive(TRUE); + mi->show(); + append(*mi); + + /* Set Clip */ + mi = Gtk::manage(new Gtk::MenuItem(_("Set Cl_ip"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SetClip)); + if (ClipRefOK || MaskRefOK) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); + + /* Release Clip */ + mi = Gtk::manage(new Gtk::MenuItem(_("Release C_lip"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ReleaseClip)); + if (ClipRefOK) { + mi->set_sensitive(TRUE); + } else { + mi->set_sensitive(FALSE); + } + mi->show(); + append(*mi); + + /* Group */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Group"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateGroup)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); +} + +void ContextMenu::SelectSameFillStroke() +{ + sp_select_same_fill_stroke_style(_desktop, true, true, true); +} + +void ContextMenu::SelectSameFillColor() +{ + sp_select_same_fill_stroke_style(_desktop, true, false, false); +} + +void ContextMenu::SelectSameStrokeColor() +{ + sp_select_same_fill_stroke_style(_desktop, false, true, false); +} + +void ContextMenu::SelectSameStrokeStyle() +{ + sp_select_same_fill_stroke_style(_desktop, false, false, true); +} + +void ContextMenu::SelectSameObjectType() +{ + sp_select_same_object_type(_desktop); +} + +void ContextMenu::ItemProperties() +{ + _desktop->selection->set(_item); + _desktop->_dlg_mgr->showDialog("ObjectProperties"); +} + +void ContextMenu::ItemSelectThis() +{ + _desktop->selection->set(_item); +} + +void ContextMenu::ItemMoveTo() +{ + Inkscape::UI::Dialogs::LayerPropertiesDialog::showMove(_desktop, _desktop->currentLayer()); +} + + + +void ContextMenu::ItemCreateLink() +{ + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:a"); + _item->parent->getRepr()->addChild(repr, _item->getRepr()); + SPObject *object = _item->document->getObjectByRepr(repr); + g_return_if_fail(SP_IS_ANCHOR(object)); + + const char *id = _item->getRepr()->attribute("id"); + Inkscape::XML::Node *child = _item->getRepr()->duplicate(xml_doc); + _item->deleteObject(false); + repr->addChild(child, nullptr); + child->setAttribute("id", id); + + Inkscape::GC::release(repr); + Inkscape::GC::release(child); + + Inkscape::DocumentUndo::done(object->document, SP_VERB_NONE, _("Create link")); + + _desktop->selection->set(SP_ITEM(object)); + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +void ContextMenu::SetMask() +{ + _desktop->selection->setMask(false, false); +} + +void ContextMenu::ReleaseMask() +{ + Inkscape::LivePathEffect::sp_remove_powermask(_desktop->selection); + _desktop->selection->unsetMask(false); +} + +void ContextMenu::CreateGroupClip() +{ + _desktop->selection->setClipGroup(); +} + +void ContextMenu::SetClip() +{ + _desktop->selection->setMask(true, false); +} + + +void ContextMenu::ReleaseClip() +{ + Inkscape::LivePathEffect::sp_remove_powerclip(_desktop->selection); + _desktop->selection->unsetMask(true); +} + +void ContextMenu::MakeGroupMenu() +{ + /* Ungroup */ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Ungroup"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateUngroup)); + mi->show(); + append(*mi); +} + +void ContextMenu::ActivateGroup() +{ + _desktop->selection->group(); +} + +void ContextMenu::ActivateUngroup() +{ + std::vector children; + + sp_item_group_ungroup(static_cast(_item), children); + _desktop->selection->setList(children); +} + +void ContextMenu::ActivateUngroupPopSelection() +{ + _desktop->selection->popFromGroup(); +} + + +void ContextMenu::MakeAnchorMenu() +{ + Gtk::MenuItem* mi; + + /* Link dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("Link _Properties..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkProperties)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Select item */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Follow Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkFollow)); + mi->show(); + append(*mi); + + /* Reset transformations */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Remove Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkRemove)); + mi->show(); + append(*mi); +} + +void ContextMenu::AnchorLinkProperties() +{ + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +void ContextMenu::AnchorLinkFollow() +{ + + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + // Opening the selected links with a python extension + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.follow_link" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::AnchorLinkRemove() +{ + std::vector children; + sp_item_group_ungroup(static_cast(_item), children, false); + Inkscape::DocumentUndo::done(_desktop->doc(), SP_VERB_NONE, _("Remove link")); +} + +void ContextMenu::MakeImageMenu () +{ + Gtk::MenuItem* mi; + Inkscape::XML::Node *ir = _object->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + + /* Image properties */ + mi = Gtk::manage(new Gtk::MenuItem(_("Image _Properties..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageProperties)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Edit externally */ + mi = Gtk::manage(new Gtk::MenuItem(_("Edit Externally..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageEdit)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) { + mi->set_sensitive( FALSE ); + } + + /* Trace Bitmap */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Trace Bitmap..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageTraceBitmap)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } + + /* Embed image */ + if (Inkscape::Verb::getbyid( "org.inkscape.filter.selected.embed_image" )) { + mi = Gtk::manage(new Gtk::MenuItem(C_("Context menu", "Embed Image"))); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageEmbed)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) { + mi->set_sensitive( FALSE ); + } + } + + /* Extract image */ + if (Inkscape::Verb::getbyid( "org.inkscape.filter.extract_image" )) { + mi = Gtk::manage(new Gtk::MenuItem(C_("Context menu", "Extract Image..."))); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageExtract)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) != 0)) ) { + mi->set_sensitive( FALSE ); + } + } +} + +void ContextMenu::ImageProperties() +{ + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +Glib::ustring ContextMenu::getImageEditorName(bool is_svg) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value; + if (!is_svg) { + Glib::ustring choices = prefs->getString("/options/bitmapeditor/value"); + if (!choices.empty()) { + value = choices; + } + else { + value = "gimp"; + } + } else { + Glib::ustring choices = prefs->getString("/options/svgeditor/value"); + if (!choices.empty()) { + value = choices; + } + else { + value = "inkscape"; + } + } + return value; +} + +void ContextMenu::ImageEdit() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + GError* errThing = nullptr; + Glib::ustring bmpeditor = getImageEditorName(); + Glib::ustring cmdline = bmpeditor; + Glib::ustring name; + Glib::ustring fullname; + +#ifdef _WIN32 + // g_spawn_command_line_sync parsing is done according to Unix shell rules, + // not Windows command interpreter rules. Thus we need to enclose the + // executable path with single quotes. + int index = cmdline.find(".exe"); + if ( index < 0 ) index = cmdline.find(".bat"); + if ( index < 0 ) index = cmdline.find(".com"); + if ( index >= 0 ) { + Glib::ustring editorBin = cmdline.substr(0, index + 4).c_str(); + Glib::ustring args = cmdline.substr(index + 4, cmdline.length()).c_str(); + editorBin.insert(0, "'"); + editorBin.append("'"); + cmdline = editorBin; + cmdline.append(args); + } else { + // Enclose the whole command line if no executable path can be extracted. + cmdline.insert(0, "'"); + cmdline.append("'"); + } +#endif + + auto itemlist= _desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + Inkscape::XML::Node *ir = (*i)->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + + if (strncmp (href,"file:",5) == 0) { + // URI to filename conversion + name = g_filename_from_uri(href, nullptr, nullptr); + } else { + name.append(href); + } + + if (Glib::path_is_absolute(name)) { + fullname = name; + } else if (SP_ACTIVE_DOCUMENT->getDocumentBase()) { + fullname = Glib::build_filename(SP_ACTIVE_DOCUMENT->getDocumentBase(), name); + } else { + fullname = Glib::build_filename(Glib::get_current_dir(), name); + } + if (name.substr(name.find_last_of(".") + 1) == "SVG" || + name.substr(name.find_last_of(".") + 1) == "svg" ) + { + cmdline.erase(0, bmpeditor.length()); + Glib::ustring svgeditor = getImageEditorName(true); + cmdline = svgeditor.append(cmdline); + } + cmdline.append(" '"); + cmdline.append(fullname.c_str()); + cmdline.append("'"); + } + + //g_warning("##Command line: %s\n", cmdline.c_str()); + + g_spawn_command_line_async(cmdline.c_str(), &errThing); + + if ( errThing ) { + g_warning("Problem launching editor (%d). %s", errThing->code, errThing->message); + (_desktop->messageStack())->flash(Inkscape::ERROR_MESSAGE, errThing->message); + g_error_free(errThing); + errThing = nullptr; + } +} + +void ContextMenu::ImageTraceBitmap() +{ + INKSCAPE.dialogs_unhide(); + _desktop->_dlg_mgr->showDialog("Trace"); +} + +void ContextMenu::ImageEmbed() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.filter.selected.embed_image" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::ImageExtract() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.filter.extract_image" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::MakeShapeMenu () +{ + Gtk::MenuItem* mi; + + /* Item dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Fill and Stroke..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::FillSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); +} + +void ContextMenu::FillSettings() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("FillAndStroke"); +} + +void ContextMenu::MakeTextMenu () +{ + Gtk::MenuItem* mi; + + /* Fill and Stroke dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Fill and Stroke..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::FillSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Edit Text dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Text and Font..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::TextSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); + +#if HAVE_ASPELL + /* Spellcheck dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("Check Spellin_g..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SpellcheckSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); +#endif +} + +void ContextMenu::TextSettings () +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("TextFont"); +} + +void ContextMenu::SpellcheckSettings () +{ +#if HAVE_ASPELL + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("SpellCheck"); +#endif +} + +void ContextMenu::ShiftIcons() +{ + static auto provider = Gtk::CssProvider::create(); + static bool provider_added = false; + + Gtk::MenuItem *menuitem = nullptr; + Gtk::Box *content = nullptr; + Gtk::Image *icon = nullptr; + + static int current_shift = 0; + int calculated_shift = 0; + + // install CssProvider for our custom styles + if (!provider_added) { + auto const screen = Gdk::Screen::get_default(); + Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + provider_added = true; + } + + // get the first MenuItem with an image (i.e. "ImageMenuItem" as named below) + std::vector children = get_children(); + for (auto child: children) { + if (child->get_name() == "ImageMenuItem") { + menuitem = static_cast(child); + content = static_cast(menuitem->get_child()); + icon = static_cast(content->get_children()[0]); + break; + } + } + + // calculate how far we have to shift the icon to fit it into the empty space between menuitem and its content + if (icon) { + auto allocation_menuitem = menuitem->get_allocation(); + auto allocation_icon = icon->get_allocation(); + + if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) { + calculated_shift = allocation_menuitem.get_width() - allocation_icon.get_x() - allocation_icon.get_width(); + } else { + calculated_shift = -allocation_icon.get_x(); + } + } + + // install CSS to shift icon, use a threshold to avoid overly frequent updates + // (gtk's own calculations for the reserved space are off by a few pixels if there is no check/radio item in a menu) + if (calculated_shift && std::abs(current_shift - calculated_shift) > 2) { + current_shift = calculated_shift; + + std::string css_str; + if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) { + css_str = "#ImageMenuItem image {margin-right:" + std::to_string(-calculated_shift) + "px;}"; + } else { + css_str = "#ImageMenuItem image {margin-left:" + std::to_string(calculated_shift) + "px;}"; + } + provider->load_from_data(css_str); + } +} + +/* + 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 : diff --git a/src/ui/contextmenu.h b/src/ui/contextmenu.h new file mode 100644 index 0000000..0e1bcff --- /dev/null +++ b/src/ui/contextmenu.h @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONTEXTMENU_H +#define SEEN_CONTEXTMENU_H + +/* + * Context menu + * + * Authors: + * Lauris Kaplinski + * Frank Felfe + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +class SPDesktop; +class SPItem; +class SPObject; + +namespace Gtk { +class SeparatorMenuItem; +} + +namespace Inkscape { +class Verb; +} + +/** + * Implements the Inkscape context menu. + * + * For the context menu implementation, the ContextMenu class stores the object + * that was selected in a private data member. This should be fairly safe to do + * and a pointer to the SPItem as well as SPObject class are kept. + * All callbacks of the context menu entries are implemented as private + * functions. + * + * @todo add callbacks to destroy the context menu when it is closed (=key or mouse button pressed out of the scope of the context menu) + */ +class ContextMenu : public Gtk::Menu +{ + public: + /** + * The ContextMenu constructor contains all code to create and show the + * menu entries (aka child widgets). + * + * @param desktop pointer to the desktop the user is currently working on. + * @param item SPItem pointer to the object selected at the time the ContextMenu is created. + */ + ContextMenu(SPDesktop *desktop, SPItem *item); + ~ContextMenu() override; + + /** + * install CSS to shift menu icons into the space reserved for toggles (i.e. check and radio items) + * + * TODO: This should be private but we already re-use this code in ui/interface.cpp which is not c++ified yet. + * In future ContextMenu and the (to be created) class for the menu bar should then be derived from one common base class. + */ + void ShiftIcons(); + private: + SPItem *_item; // pointer to the object selected at the time the ContextMenu is created + SPObject *_object; // pointer to the object selected at the time the ContextMenu is created + SPDesktop *_desktop; //pointer to the desktop the user was currently working on at the time the ContextMenu is created + + int positionOfLastDialog; + + Gtk::MenuItem MIGroup; //menu entry to enter a group + Gtk::MenuItem MIParent; //menu entry to leave a group + + /** + * auxiliary function that adds a separator line in the context menu + */ + Gtk::SeparatorMenuItem* AddSeparator(); + + /** + * Appends a custom menu UI from a verb. + * + * c++ified version of sp_ui_menu_append_item. + * @see sp_ui_menu_append_item_from_verb and synchronize/drop that function when c++ifying other code in interface.cpp + * + * @param show_icon True if an icon should be displayed before the menu item's label + * + */ + void AppendItemFromVerb(Inkscape::Verb *verb, bool show_icon = false); + + /** + * main function which is responsible for creating the context sensitive menu items, + * calls subfunctions below to create the menu entry widgets. + */ + void MakeObjectMenu (); + /** + * creates menu entries for an SP_TYPE_ITEM object + */ + void MakeItemMenu (); + /** + * creates menu entries for a grouped object + */ + void MakeGroupMenu (); + /** + * creates menu entries for an anchor object + */ + void MakeAnchorMenu (); + /** + * creates menu entries for a bitmap image object + */ + void MakeImageMenu (); + /** + * creates menu entries for a shape object + */ + void MakeShapeMenu (); + /** + * creates menu entries for a text object + */ + void MakeTextMenu (); + + void EnterGroup(Gtk::MenuItem* mi); + void LeaveGroup(); + void LockSelected(); + void HideSelected(); + void UnLockBelow(std::vector items); + void UnHideBelow(std::vector items); + ////////////////////////////////////////// + //callbacks for the context menu entries of an SP_TYPE_ITEM object + void ItemProperties(); + void ItemSelectThis(); + void ItemMoveTo(); + void SelectSameFillStroke(); + void SelectSameFillColor(); + void SelectSameStrokeColor(); + void SelectSameStrokeStyle(); + void SelectSameObjectType(); + void ItemCreateLink(); + void CreateGroupClip(); + void SetMask(); + void ReleaseMask(); + void SetClip(); + void ReleaseClip(); + ////////////////////////////////////////// + + + /** + * callback, is executed on clicking the anchor "Group" and "Ungroup" menu entry + */ + void ActivateUngroupPopSelection(); + void ActivateUngroup(); + void ActivateGroup(); + + void AnchorLinkProperties(); + /** + * placeholder for callback to be executed on clicking the anchor "Follow link" context menu entry + * @todo add code to follow link externally + */ + void AnchorLinkFollow(); + + /** + * callback, is executed on clicking the anchor "Link remove" menu entry + */ + void AnchorLinkRemove(); + + + /** + * callback, opens the image properties dialog and is executed on clicking the context menu entry with similar name + */ + void ImageProperties(); + + /** + * callback, is executed on clicking the image "Edit Externally" menu entry + */ + void ImageEdit(); + + /** + * auxiliary function that loads the external image editor name from the settings. + */ + Glib::ustring getImageEditorName(bool is_svg = false); + + /** + * callback, is executed on clicking the "Embed Image" menu entry + */ + void ImageEmbed(); + + /** + * callback, is executed on clicking the "Trace Bitmap" menu entry + */ + void ImageTraceBitmap(); + + /** + * callback, is executed on clicking the "Extract Image" menu entry + */ + void ImageExtract(); + + + /** + * callback, is executed on clicking the "Fill and Stroke" menu entry + */ + void FillSettings(); + + + /** + * callback, is executed on clicking the "Text and Font" menu entry + */ + void TextSettings(); + + /** + * callback, is executed on clicking the "Check spelling" menu entry + */ + void SpellcheckSettings(); +}; +#endif // SEEN_CONTEXT_MENU_H + +/* + 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 : diff --git a/src/ui/control-manager.cpp b/src/ui/control-manager.cpp new file mode 100644 index 0000000..0330b5b --- /dev/null +++ b/src/ui/control-manager.cpp @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Central facade for accessing and managing on-canvas controls. + * + * Author: + * Jon A. Cruz + * + * Copyright 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "control-manager.h" + +#include +#include + +#include + +#include "display/sodipodi-ctrl.h" // for SP_TYPE_CTRL +#include "display/sp-ctrlline.h" +#include "display/sp-ctrlcurve.h" +#include "preferences.h" + +using Inkscape::ControlFlags; + +namespace { + +// Note: The following operator overloads are local to this file at the moment to discourage flag manipulation elsewhere. +/* +ControlFlags operator |(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +*/ + +ControlFlags operator &(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +ControlFlags operator ^(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast(static_cast(lhs) ^ static_cast(rhs)); +} + +ControlFlags& operator ^=(ControlFlags &lhs, ControlFlags rhs) +{ + lhs = lhs ^ rhs; + return lhs; +} + +} // namespace + +// Default color for line: +#define LINE_COLOR_PRIMARY 0x0000ff7f +#define LINE_COLOR_SECONDARY 0xff00007f +#define LINE_COLOR_TERTIARY 0xffff007f + +namespace Inkscape { + +class ControlManagerImpl +{ +public: + ControlManagerImpl(ControlManager &manager); + + ~ControlManagerImpl() = default; + + SPCanvasItem *createControl(SPCanvasGroup *parent, ControlType type); + + void setControlSize(int size, bool force = false); + + void track(SPCanvasItem *anchor); + + sigc::connection connectCtrlSizeChanged(const sigc::slot &slot); + + void updateItem(SPCanvasItem *item); + + bool setControlType(SPCanvasItem *item, ControlType type); + + bool setControlResize(SPCanvasItem *item, int ctrlResize); + + void setSelected(SPCanvasItem *item, bool selected); + +private: + static void thingFinalized(gpointer data, GObject *wasObj); + + void thingFinalized(GObject *wasObj); + + class PrefListener : public Inkscape::Preferences::Observer + { + public: + PrefListener(ControlManagerImpl &manager) : Inkscape::Preferences::Observer("/options/grabsize/value"), _mgr(manager) {} + ~PrefListener() override = default; + + void notify(Inkscape::Preferences::Entry const &val) override { + int size = val.getIntLimited(3, 1, 7); + _mgr.setControlSize(size); + } + + ControlManagerImpl &_mgr; + }; + + + ControlManager &_manager; + sigc::signal _sizeChangedSignal; + PrefListener _prefHook; + int _size; // Size from the grabsize preference + int _resize; // Way size should change from grabsize + std::vector _itemList; + std::map > _sizeTable; + std::map _typeTable; + std::map _ctrlToShape; + std::set _resizeOnSelect; +}; + +ControlManagerImpl::ControlManagerImpl(ControlManager &manager) : + _manager(manager), + _sizeChangedSignal(), + _prefHook(*this), + _size(3), + _resize(3), + _itemList(), + _sizeTable() +{ + _typeTable[CTRL_TYPE_UNKNOWN] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_LPE] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_ADJ_HANDLE] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_ANCHOR] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_INVISIPOINT] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_AUTO] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_CUSP] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_SMOOTH] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_SYMETRICAL] = SP_TYPE_CTRL; + + _typeTable[CTRL_TYPE_LINE] = SP_TYPE_CTRLLINE; + + // ------- + _ctrlToShape[CTRL_TYPE_UNKNOWN] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_LPE] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_NODE_CUSP] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_NODE_SMOOTH] = SP_CTRL_SHAPE_SQUARE; + _ctrlToShape[CTRL_TYPE_NODE_AUTO] = SP_CTRL_SHAPE_CIRCLE; + _ctrlToShape[CTRL_TYPE_NODE_SYMETRICAL] = SP_CTRL_SHAPE_SQUARE; + + _ctrlToShape[CTRL_TYPE_ADJ_HANDLE] = SP_CTRL_SHAPE_CIRCLE; + _ctrlToShape[CTRL_TYPE_INVISIPOINT] = SP_CTRL_SHAPE_SQUARE; + + // ------- + + _resizeOnSelect.insert(CTRL_TYPE_NODE_AUTO); + _resizeOnSelect.insert(CTRL_TYPE_NODE_CUSP); + _resizeOnSelect.insert(CTRL_TYPE_NODE_SMOOTH); + _resizeOnSelect.insert(CTRL_TYPE_NODE_SYMETRICAL); + + // ------- + + // The size of the controls is determined by the grabsize preference; see the "Handle size" parameter in + // the input/output group, on the "input devices" tab; this parameter ranges from 1 to 7; When selecting a control, we + // increase the size by an additional 2 pixels, if _resizeOnSelect is true (see setSelected()) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->addObserver(_prefHook); + + _size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 7); + + // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids + // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the + // control has an even width). + { + unsigned int sizes[] = {7, 7, 7, 7, 7, 7, 7}; + _sizeTable[CTRL_TYPE_UNKNOWN] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_ADJ_HANDLE] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_ANCHOR] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17}; + _sizeTable[CTRL_TYPE_POINT] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_ROTATE] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_SIZER] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_SHAPER] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17}; + _sizeTable[CTRL_TYPE_NODE_AUTO] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_NODE_CUSP] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_NODE_SMOOTH] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_NODE_SYMETRICAL] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {1, 1, 1, 1, 1, 1, 1}; + _sizeTable[CTRL_TYPE_INVISIPOINT] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = { 5, 7, 9, 11, 13, 15, 17 }; + _sizeTable[CTRL_TYPE_LPE] = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } +} + + +void ControlManagerImpl::setControlSize(int size, bool force) +{ + if ((size < 1) || (size > 7)) { + g_warning("Illegal logical size set: %d", size); + } else if (force || (size != _size)) { + _size = size; + + for (auto & it : _itemList) + { + if (it) { + updateItem(it); + } + } + + //_sizeChangedSignal.emit(); + } +} + +SPCanvasItem *ControlManagerImpl::createControl(SPCanvasGroup *parent, ControlType type) +{ + SPCanvasItem *item = nullptr; + unsigned int targetSize = _sizeTable[type][_size - 1]; + switch (type) + { + case CTRL_TYPE_ADJ_HANDLE: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape",SP_CTRL_SHAPE_CIRCLE, + "size", targetSize, + "filled", 1, + "fill_color", 0xffffff7f, + "stroked", 1, + "stroke_color", 0x0000ff7f, + NULL); + break; + case CTRL_TYPE_ANCHOR: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "size", targetSize, + "filled", 1, + "fill_color", 0xffffff7f, + "stroked", 1, + "stroke_color", 0x000000ff, + NULL); + break; + case CTRL_TYPE_NODE_AUTO: + case CTRL_TYPE_NODE_CUSP: + case CTRL_TYPE_NODE_SMOOTH: + case CTRL_TYPE_NODE_SYMETRICAL: + { + SPCtrlShapeType shape = _ctrlToShape[_ctrlToShape.count(type) ? type : CTRL_TYPE_UNKNOWN]; + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape", shape, + "size", targetSize, + NULL); + break; + } + case CTRL_TYPE_INVISIPOINT: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape", SP_CTRL_SHAPE_SQUARE, + "size", targetSize, + NULL); + break; + case CTRL_TYPE_UNKNOWN: + case CTRL_TYPE_LPE: + default: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, nullptr); + } + if (item) { + item->ctrlType = type; + } + return item; +} + +void ControlManagerImpl::track(SPCanvasItem *item) +{ + g_object_weak_ref( G_OBJECT(item), ControlManagerImpl::thingFinalized, this ); + + _itemList.push_back(item); + + setControlSize(_size, true); +} + +sigc::connection ControlManagerImpl::connectCtrlSizeChanged(const sigc::slot &slot) +{ + return _sizeChangedSignal.connect(slot); +} + +void ControlManagerImpl::updateItem(SPCanvasItem *item) +{ + if (item) { + unsigned int target = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", target, NULL); + + sp_canvas_item_request_update(item); + } +} + +bool ControlManagerImpl::setControlType(SPCanvasItem *item, ControlType type) +{ + bool accepted = false; + if (item && (item->ctrlType == type)) { + // nothing to do + accepted = true; + } else if (item) { + if (_ctrlToShape.count(type) && (_typeTable[type] == _typeTable[item->ctrlType])) { // compatible? + unsigned int targetSize = _sizeTable[type][_size - 1] + item->ctrlResize; + SPCtrlShapeType targetShape = _ctrlToShape[type]; + + g_object_set(item, "shape", targetShape, "size", targetSize, NULL); + item->ctrlType = type; + accepted = true; + } + } + + return accepted; +} + +bool ControlManagerImpl::setControlResize(SPCanvasItem *item, int ctrlResize) +{ + // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids + // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the + // control has an even width). ctrlResize should therefore be an even number, such that the sum (targetSize) + // is also odd + if(item) { + item->ctrlResize = ctrlResize; + unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", targetSize, NULL); + return true; + } + return false; +} + +void ControlManagerImpl::setSelected(SPCanvasItem *item, bool selected) +{ + if (_manager.isSelected(item) != selected) { + item->ctrlFlags ^= CTRL_FLAG_SELECTED; // toggle, since we know it is different + + if (selected && _resizeOnSelect.count(item->ctrlType)) { + item->ctrlResize = 2; + } else { + item->ctrlResize = 0; + } + + // TODO refresh colors + unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", targetSize, NULL); + } +} + +void ControlManagerImpl::thingFinalized(gpointer data, GObject *wasObj) +{ + if (data) { + reinterpret_cast(data)->thingFinalized(wasObj); + } +} + +void ControlManagerImpl::thingFinalized(GObject *wasObj) +{ + SPCanvasItem *wasItem = reinterpret_cast(wasObj); + if (wasItem) + { + std::vector::iterator it = std::find(_itemList.begin(), _itemList.end(), wasItem); + if (it != _itemList.end()) + { + _itemList.erase(it); + } + } +} + + +// ---------------------------------------------------- + +ControlManager::ControlManager() : + _impl(new ControlManagerImpl(*this)) +{ +} + +ControlManager::~ControlManager() += default; + +ControlManager &ControlManager::getManager() +{ + static ControlManager instance; + + return instance; +} + + +SPCanvasItem *ControlManager::createControl(SPCanvasGroup *parent, ControlType type) +{ + return _impl->createControl(parent, type); +} + +SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, CtrlLineType type) +{ + SPCtrlLine *line = SP_CTRLLINE(sp_canvas_item_new(parent, SP_TYPE_CTRLLINE, nullptr)); + if (line) { + line->ctrlType = CTRL_TYPE_LINE; + + line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY : + (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY); + line->setCoords(0, 0, 0, 0); + } + return line; +} + +SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, Geom::Point const &p1, Geom::Point const &p2, CtrlLineType type) +{ + SPCtrlLine *line = createControlLine(parent, type); + if (line) { + line->setCoords(p1, p2); + } + return line; +} + +SPCtrlCurve *ControlManager::createControlCurve(SPCanvasGroup *parent, Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3, CtrlLineType type) +{ + SPCtrlCurve *line = SP_CTRLCURVE(sp_canvas_item_new(parent, SP_TYPE_CTRLCURVE, nullptr)); + if (line) { + line->ctrlType = CTRL_TYPE_LINE; + + line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY : + (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY); + line->setCoords(p0, p1, p2, p3); + } + return line; +} + +void ControlManager::track(SPCanvasItem *item) +{ + _impl->track(item); +} + +sigc::connection ControlManager::connectCtrlSizeChanged(const sigc::slot &slot) +{ + return _impl->connectCtrlSizeChanged(slot); +} + +void ControlManager::updateItem(SPCanvasItem *item) +{ + return _impl->updateItem(item); +} + +bool ControlManager::setControlType(SPCanvasItem *item, ControlType type) +{ + return _impl->setControlType(item, type); +} + +bool ControlManager::setControlResize(SPCanvasItem *item, int ctrlResize) +{ + return _impl->setControlResize(item, ctrlResize); +} + +bool ControlManager::isActive(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_ACTIVE) != 0; +} + +void ControlManager::setActive(SPCanvasItem *item, bool active) +{ + if (isActive(item) != active) { + item->ctrlFlags ^= CTRL_FLAG_ACTIVE; // toggle, since we know it is different + // TODO refresh size/colors + } +} + +bool ControlManager::isPrelight(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_PRELIGHT) != 0; +} + +void ControlManager::setPrelight(SPCanvasItem *item, bool prelight) +{ + if (isPrelight(item) != prelight) { + item->ctrlFlags ^= CTRL_FLAG_PRELIGHT; // toggle, since we know it is different + // TODO refresh size/colors + } +} + +bool ControlManager::isSelected(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_SELECTED) != 0; +} + +void ControlManager::setSelected(SPCanvasItem *item, bool selected) +{ + _impl->setSelected(item, selected); +} + +} // 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 : diff --git a/src/ui/control-manager.h b/src/ui/control-manager.h new file mode 100644 index 0000000..12ac19a --- /dev/null +++ b/src/ui/control-manager.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ControlManager - Coordinates creation and styling of nodes, handles, etc. + * + * Author: + * Jon A. Cruz + * + * Copyright 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_CONTROL_MANAGER_H +#define SEEN_INKSCAPE_CONTROL_MANAGER_H + +#include +#include + +#include "ui/control-types.h" + +struct SPCanvasGroup; +struct SPCanvasItem; +struct SPCtrlLine; +struct SPCtrlCurve; + +namespace Geom +{ + +class Point; + +} // namespace Geom + +namespace Inkscape { + +enum CtrlLineType { + CTLINE_PRIMARY, + CTLINE_SECONDARY, + CTLINE_TERTIARY, +}; + + +class ControlManagerImpl; + +class ControlManager +{ +public: + + static ControlManager &getManager(); + + ~ControlManager(); + + sigc::connection connectCtrlSizeChanged(const sigc::slot &slot); + + SPCanvasItem *createControl(SPCanvasGroup *parent, ControlType type); + + SPCtrlLine *createControlLine(SPCanvasGroup *parent, CtrlLineType type = CTLINE_PRIMARY); + + SPCtrlLine *createControlLine(SPCanvasGroup *parent, Geom::Point const &p1, Geom::Point const &p2, CtrlLineType type = CTLINE_PRIMARY); + + SPCtrlCurve *createControlCurve(SPCanvasGroup *parent, Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3, CtrlLineType type = CTLINE_PRIMARY); + + void track(SPCanvasItem *item); + + void updateItem(SPCanvasItem *item); + + bool setControlType(SPCanvasItem *item, ControlType type); + + bool setControlResize(SPCanvasItem *item, int ctrlResize); + + bool isActive(SPCanvasItem *item) const; + void setActive(SPCanvasItem *item, bool active); + + bool isPrelight(SPCanvasItem *item) const; + void setPrelight(SPCanvasItem *item, bool prelight); + + bool isSelected(SPCanvasItem *item) const; + void setSelected(SPCanvasItem *item, bool selected); + +private: + ControlManager(); + std::unique_ptr _impl; + friend class ControlManagerImpl; +}; + +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_CONTROL_MANAGER_H +/* + 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 : diff --git a/src/ui/control-types.h b/src/ui/control-types.h new file mode 100644 index 0000000..896ccf8 --- /dev/null +++ b/src/ui/control-types.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_CONTROL_TYPES_H +#define SEEN_UI_CONTROL_TYPES_H + +/* + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2012 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +namespace Inkscape +{ + +// Rough initial set. Most likely needs refinement. +enum ControlType { + CTRL_TYPE_UNKNOWN, + CTRL_TYPE_ADJ_HANDLE, + CTRL_TYPE_ANCHOR, + CTRL_TYPE_POINT, + CTRL_TYPE_ROTATE, + CTRL_TYPE_SIZER, + CTRL_TYPE_SHAPER, + CTRL_TYPE_LINE, + CTRL_TYPE_LPE, + CTRL_TYPE_NODE_AUTO, + CTRL_TYPE_NODE_CUSP, + CTRL_TYPE_NODE_SMOOTH, + CTRL_TYPE_NODE_SYMETRICAL, + CTRL_TYPE_INVISIPOINT +}; + +/** + * Flags for internal representation/tracking. + */ +enum ControlFlags { + CTRL_FLAG_NORMAL = 0, + CTRL_FLAG_ACTIVE = 1 << 0, + CTRL_FLAG_PRELIGHT = 1 << 1, + CTRL_FLAG_SELECTED = 1 << 2, +}; + +} // namespace Inkscape + +#endif // SEEN_UI_CONTROL_TYPES_H + +/* + 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 : diff --git a/src/ui/desktop/README b/src/ui/desktop/README new file mode 100644 index 0000000..e2932ca --- /dev/null +++ b/src/ui/desktop/README @@ -0,0 +1,27 @@ + + +This directory contains code related to the Inkscape desktop, that is +code that is directly used by the InkscapeWindow class and in linking +the desktop to the canvas. It should not contain basic widgets, +dialogs, toolbars, etc. + +To do: + +* widgets/desktop-widget.h/cpp should disappear with code ending up in either + InkscapeWindow.h/cpp or desktop.h/cpp (or in new files). + +* ui/view/view-widget.h/cpp should disappear ('view' should be member of window) + +* desktop.h/cpp should only contain code that links the desktop to the canvas. + +* Convert GUI to use actions where possible. + +* Future Structure: + Main menu bar (menubar.h/.cpp) + Tool bar + Multipaned widget containing + Dialogs + Tools + Canvas + Palette (maybe turn into dialog). + Status bar diff --git a/src/ui/desktop/menubar.cpp b/src/ui/desktop/menubar.cpp new file mode 100644 index 0000000..68c989a --- /dev/null +++ b/src/ui/desktop/menubar.cpp @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Desktop main menu bar code. + */ +/* + * Authors: + * Tavmjong Bah + * Alex Valavanis + * Patrick Storz + * Krzysztof Kosiński + * Kris De Gussem + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#include +#include + +#include + +#include "inkscape.h" +#include "inkscape-application.h" // Open recent + +#include "message-context.h" +#include "shortcuts.h" + +#include "helper/action.h" +#include "helper/action-context.h" + +#include "object/sp-namedview.h" + +#include "ui/contextmenu.h" // Shift to make room for icons +#include "ui/icon-loader.h" +#include "ui/view/view.h" +#include "ui/uxmanager.h" // To Do: Convert to actions + +#ifdef GDK_WINDOWING_QUARTZ +#include +#endif + +// ================= Common ==================== + +std::vector, Inkscape::UI::View::View *>> menuitems; +unsigned int lastverb = -1; + + +/** + * Get menu item (if it was registered in `menuitems`) + */ +Gtk::MenuItem *get_menu_item_for_verb(unsigned int verb, Inkscape::UI::View::View *view) +{ + for (auto &item : menuitems) { + if (item.first.first == verb && item.second == view) { + return item.first.second; + } + } + return nullptr; +} + +#ifdef GDK_WINDOWING_QUARTZ +/** + * Update menubar for macOS + * + * Can be used as a glib timeout callback. + */ +static gboolean sync_menubar(gpointer = nullptr) +{ + auto osxapp = gtkosx_application_get(); + if (osxapp) { + gtkosx_application_sync_menubar(osxapp); + } + return false; +} +#endif + +// Sets tip +static void +select_action(SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, action->tip); +} + +// Clears tip +static void +deselect_action(SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->clear(); +} + +// Trigger action +static void item_activate(Gtk::MenuItem *menuitem, SPAction *action) +{ + if (action->verb->get_code() == lastverb) { + lastverb = -1; + return; + } + lastverb = action->verb->get_code(); + sp_action_perform(action, nullptr); + lastverb = -1; + +#ifdef GDK_WINDOWING_QUARTZ + sync_menubar(); +#endif +} + +static void set_menuitems(unsigned int emitting_verb, bool value) +{ + for (auto menu : menuitems) { + if (menu.second == SP_ACTIVE_DESKTOP) { + if (emitting_verb == menu.first.first) { + if (emitting_verb == lastverb) { + lastverb = -1; + return; + } + lastverb = emitting_verb; + Gtk::CheckMenuItem *check = dynamic_cast(menu.first.second); + Gtk::RadioMenuItem *radio = dynamic_cast(menu.first.second); + if (radio) { + radio->property_active() = value; + } else if (check) { + check->property_active() = value; + } + lastverb = -1; + } + } + } +} + +// Change label name (used in the Undo/Redo menu items). +static void +set_name(Glib::ustring const &name, Gtk::MenuItem* menuitem) +{ + if (menuitem) { + Gtk::Widget* widget = menuitem->get_child(); + + // Label is either child of menuitem + Gtk::Label* label = dynamic_cast(widget); + + // Or wrapped inside a box which is a child of menuitem (if with icon). + if (!label) { + Gtk::Box* box = dynamic_cast(widget); + if (box) { + std::vector children = box->get_children(); + for (auto child: children) { + label = dynamic_cast(child); + if (label) break; + } + } + } + + if (label) { + label->set_markup_with_mnemonic(name); + } else { + std::cerr << "set_name: could not find label!" << std::endl; + } + } +} + +/* Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + * + * TODO: This code already exists as a C++ version in the class ContextMenu so we can simply wrap + * it here. In future ContextMenu and the (to be created) class for the menu bar should then + * be derived from one common base class. + * + * TODO: This code is called everytime a menu is opened. We can certainly find a way to call it once. + */ +static void +shift_icons(Gtk::Menu* menu) +{ + ContextMenu *contextmenu = static_cast(menu); + contextmenu->ShiftIcons(); +} + +// ================= MenuItem ==================== + +Gtk::MenuItem* +build_menu_item_from_verb(SPAction* action, + bool show_icon, + bool radio = false, + Gtk::RadioMenuItem::Group *group = nullptr) +{ + Gtk::MenuItem* menuitem = nullptr; + + if (radio) { + menuitem = Gtk::manage(new Gtk::RadioMenuItem(*group)); + } else { + menuitem = Gtk::manage(new Gtk::MenuItem()); + } + + Gtk::AccelLabel* label = Gtk::manage(new Gtk::AccelLabel(action->name, true)); + label->set_xalign(0.0); + label->set_accel_widget(*menuitem); + sp_shortcut_add_accelerator((GtkWidget*)menuitem->gobj(), sp_shortcut_get_primary(action->verb)); + + // If there is an image associated with the action, we can add it as an icon for the menu item. + if (show_icon && action->image) { + menuitem->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image* image = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can + // only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + box->pack_start(*image, false, false, 0); + box->pack_start(*label, true, true, 0); + menuitem->add(*box); + } else { + menuitem->add(*label); + } + menuitem->signal_activate().connect( + sigc::bind(sigc::ptr_fun(&item_activate), menuitem, action)); + menuitem->signal_select().connect( sigc::bind(sigc::ptr_fun(&select_action), action)); + menuitem->signal_deselect().connect(sigc::bind(sigc::ptr_fun(&deselect_action), action)); + + action->signal_set_sensitive.connect( + sigc::bind<0>(sigc::ptr_fun(>k_widget_set_sensitive), (GtkWidget*)menuitem->gobj())); + action->signal_set_name.connect( + sigc::bind(sigc::ptr_fun(&set_name), menuitem)); + + // initialize sensitivity with verb default + sp_action_set_sensitive(action, action->verb->get_default_sensitive()); + + return menuitem; +} + +// =============== CheckMenuItem ================== + +bool getStateFromPref(SPDesktop *dt, Glib::ustring item) +{ + Glib::ustring pref_path; + + if (dt->is_focusMode()) { + pref_path = "/focus/"; + } else if (dt->is_fullscreen()) { + pref_path = "/fullscreen/"; + } else { + pref_path = "/window/"; + } + + pref_path += item; + pref_path += "/state"; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return prefs->getBool(pref_path, false); +} + +// I wonder if this can be done without hard coding names. +static void +checkitem_update(Gtk::CheckMenuItem* menuitem, SPAction* action) +{ + bool active = false; + if (action && action->id) { + Glib::ustring id = action->id; + SPDesktop* dt = static_cast(sp_action_get_view(action)); + + if (id == "ToggleGrid") { + active = dt->gridsEnabled(); + + } else if (id == "EditGuidesToggleLock") { + active = dt->namedview->lockguides; + + } else if (id == "ToggleGuides") { + active = dt->namedview->getGuides(); + + } else if (id == "ToggleRotationLock") { + active = dt->get_rotation_lock(); + + } + else if (id == "ViewCmsToggle") { + active = dt->colorProfAdjustEnabled(); + } + else if (id == "ViewSplitModeToggle") { + active = dt->splitMode(); + + } else if (id == "ViewXRayToggle") { + active = dt->xrayMode(); + + } else if (id == "ToggleCommandsToolbar") { + active = getStateFromPref(dt, "commands"); + + } else if (id == "ToggleSnapToolbar") { + active = getStateFromPref(dt, "snaptoolbox"); + + } else if (id == "ToggleToolToolbar") { + active = getStateFromPref(dt, "toppanel"); + + } else if (id == "ToggleToolbox") { + active = getStateFromPref(dt, "toolbox"); + + } else if (id == "ToggleRulers") { + active = getStateFromPref(dt, "rulers"); + + } else if (id == "ToggleScrollbars") { + active = getStateFromPref(dt, "scrollbars"); + + } else if (id == "TogglePalette") { + active = getStateFromPref(dt, "panels"); // Rename? + + } else if (id == "ToggleStatusbar") { + active = getStateFromPref(dt, "statusbar"); + + } else if (id == "FlipHorizontal") { + active = dt->is_flipped(SPDesktop::FLIP_HORIZONTAL); + + } else if (id == "FlipVertical") { + active = dt->is_flipped(SPDesktop::FLIP_VERTICAL); + + } else { + std::cerr << "checkitem_update: unhandled item: " << id << std::endl; + } + + menuitem->set_active(active); + } else { + std::cerr << "checkitem_update: unknown action" << std::endl; + } +} + +static Gtk::CheckMenuItem* +build_menu_check_item_from_verb(SPAction* action) +{ + // This does not work for some reason! + // Gtk::CheckMenuItem* menuitem = Gtk::manage(new Gtk::CheckMenuItem(action->name, true)); + // sp_shortcut_add_accelerator(GTK_WIDGET(menuitem->gobj()), sp_shortcut_get_primary(action->verb)); + + GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(action->name); + sp_shortcut_add_accelerator(item, sp_shortcut_get_primary(action->verb)); + Gtk::CheckMenuItem* menuitem = Gtk::manage(Glib::wrap(GTK_CHECK_MENU_ITEM(item))); + + // Set initial state before connecting signals. + checkitem_update(menuitem, action); + + menuitem->signal_toggled().connect( + sigc::bind(sigc::ptr_fun(&item_activate), menuitem, action)); + menuitem->signal_select().connect( sigc::bind(sigc::ptr_fun(&select_action), action)); + menuitem->signal_deselect().connect(sigc::bind(sigc::ptr_fun(&deselect_action), action)); + + return menuitem; +} + + +// ================= Tasks Submenu ============== +static void +task_activated(SPDesktop* dt, int number) +{ + Inkscape::UI::UXManager::getInstance()->setTask(dt, number); + +#ifdef GDK_WINDOWING_QUARTZ + // call later, crashes during startup if called directly + g_idle_add(sync_menubar, nullptr); +#endif +} + +// Sets tip +static void +select_task(SPDesktop* dt, Glib::ustring tip) +{ + dt->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip.c_str()); +} + +// Clears tip +static void +deselect_task(SPDesktop* dt) +{ + dt->tipsMessageContext()->clear(); +} + +static void +add_tasks(Gtk::MenuShell* menu, SPDesktop* dt) +{ + const Glib::ustring data[3][2] = { + { C_("Interface setup", "Default"), _("Default interface setup") }, + { C_("Interface setup", "Custom"), _("Setup for custom task") }, + { C_("Interface setup", "Wide"), _("Setup for widescreen work") } + }; + + int active = Inkscape::UI::UXManager::getInstance()->getDefaultTask(dt); + + Gtk::RadioMenuItem::Group group; + + for (unsigned int i = 0; i < 3; ++i) { + + Gtk::RadioMenuItem* menuitem = Gtk::manage(new Gtk::RadioMenuItem(group, data[i][0])); + if (menuitem) { + if (active == i) { + menuitem->set_active(true); + } + + menuitem->signal_toggled().connect( + sigc::bind(sigc::ptr_fun(&task_activated), dt, i)); + menuitem->signal_select().connect( + sigc::bind(sigc::ptr_fun(&select_task), dt, data[i][1])); + menuitem->signal_deselect().connect( + sigc::bind(sigc::ptr_fun(&deselect_task),dt)); + + menu->append(*menuitem); + } + } +} + + +static void +sp_recent_open(Gtk::RecentChooser* recentchooser) +{ + Glib::ustring uri = recentchooser->get_current_uri(); + + Glib::RefPtr file = Gio::File::create_for_uri(uri); + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + app->create_window(file); +} + +// =================== Main Menu ================ +// Recursively build menu and submenus. +void +build_menu(Gtk::MenuShell* menu, Inkscape::XML::Node* xml, Inkscape::UI::View::View* view, bool show_icons = true) +{ + if (menu == nullptr) { + std::cerr << "build_menu: menu is nullptr" << std::endl; + return; + } + + if (xml == nullptr) { + std::cerr << "build_menu: xml is nullptr" << std::endl; + return; + } + + // user preference for icons in menus (1: show all, -1: hide all; 0: theme chooses per icon) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_icons_pref = prefs->getInt("/theme/menuIcons", 0); + + Gtk::RadioMenuItem::Group group; + + for (auto menu_ptr = xml; menu_ptr != nullptr; menu_ptr = menu_ptr->next()) { + + if (menu_ptr->name()) { + + // show menu icons for current item? + bool show_icons_curr = show_icons; + if (show_icons_pref == 1) { // show all icons per global pref + show_icons_curr = true; + } else if (show_icons_pref == -1) { // hide all icons per global pref + show_icons_curr = false; + } else { // set according to 'show-icons' attribute in theme's XML file; value is fully inherited + const char *str = menu_ptr->attribute("show-icons"); + if (str) { + Glib::ustring ustr = str; + if (ustr == "true") { + show_icons_curr = true; + } else if (ustr == "false") { + show_icons_curr = false; + } else { + std::cerr << "build_menu: invalid value for 'show-icons' (use 'true' or 'false')." + << ustr << std::endl; + } + } + } + + Glib::ustring name = menu_ptr->name(); + + if (name == "inkscape") { + build_menu(menu, menu_ptr->firstChild(), view, show_icons_curr); + continue; + } + + if (name == "submenu") { + const char *name = menu_ptr->attribute("name"); + if (!name) { + g_warning("menus.xml: skipping submenu without name."); + continue; + } + + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_(name), true)); + Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu()); + build_menu(submenu, menu_ptr->firstChild(), view, show_icons_curr); + menuitem->set_submenu(*submenu); + menu->append(*menuitem); + + submenu->signal_map().connect( + sigc::bind(sigc::ptr_fun(&shift_icons), submenu)); + + continue; + } + + if (name == "contextmenu") { + if (menu_ptr->attribute("id")) { + Glib::ustring id = menu_ptr->attribute("id"); + if (id == "canvas" || id == "layers" || id == "objects") { + Glib::ustring prefname = Glib::ustring::compose("/theme/menuIcons_%1", id); + prefs->setBool(prefname, show_icons_curr); + } else { + std::cerr << "build_menu: invalid contextmenu id: " << id << std::endl; + } + } else { + std::cerr << "build_menu: contextmenu element needs a valid id" << std::endl; + } + continue; + } + + if (name == "verb") { + if (menu_ptr->attribute("verb-id") != nullptr) { + Glib::ustring verb_name = menu_ptr->attribute("verb-id"); + + Inkscape::Verb *verb = Inkscape::Verb::getbyid(verb_name.c_str()); + if (verb != nullptr && verb->get_code() != SP_VERB_NONE) { + + SPAction* action = verb->get_action(Inkscape::ActionContext(view)); + if (menu_ptr->attribute("check") != nullptr) { + Gtk::MenuItem *menuitem = build_menu_check_item_from_verb(action); + if (menuitem) { + std::pair, Inkscape::UI::View::View *> + verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view); + menuitems.push_back(verbmenuitem); + menu->append(*menuitem); + } + } else if (menu_ptr->attribute("radio") != nullptr) { + Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr, true, &group); + if (menuitem) { + if (menu_ptr->attribute("default") != nullptr) { + auto radiomenuitem = dynamic_cast(menuitem); + if (radiomenuitem) { + radiomenuitem->set_active(true); + } + } + std::pair, Inkscape::UI::View::View *> + verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view); + menuitems.push_back(verbmenuitem); + menu->append(*menuitem); + } + } else { + Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr); + if (menuitem) { + menu->append(*menuitem); + } + +#ifdef GDK_WINDOWING_QUARTZ + // for moving menu items to "Inkscape" menu + switch (verb->get_code()) { + case SP_VERB_DIALOG_DISPLAY: + case SP_VERB_DIALOG_INPUT: + case SP_VERB_HELP_ABOUT: + menuitems.emplace_back(std::make_pair(verb->get_code(), menuitem), view); + } +#endif + } + } else if (true +#ifndef HAVE_ASPELL + && strcmp(verb_name.c_str(), "DialogSpellcheck") != 0 +#endif + ) { + std::cerr << "build_menu: no verb with id: " << verb_name << std::endl; + } + } + continue; + } + + // This is used only for wide-screen vs non-wide-screen displays. + // The code should be rewritten to use actions like everything else here. + if (name == "task-checkboxes") { + add_tasks(menu, static_cast(view)); + continue; + } + + if (name == "recent-file-list") { + + // Filter recent files to those already opened in Inkscape. + Glib::RefPtr recentfilter = Gtk::RecentFilter::create(); + recentfilter->add_application(g_get_prgname()); + recentfilter->add_application("org.inkscape.Inkscape"); + recentfilter->add_application("inkscape"); +#ifdef _WIN32 + recentfilter->add_application("inkscape.exe"); +#endif + + Gtk::RecentChooserMenu* recentchoosermenu = Gtk::manage(new Gtk::RecentChooserMenu()); + int max = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value"); + recentchoosermenu->set_limit(max); + recentchoosermenu->set_sort_type(Gtk::RECENT_SORT_MRU); // Sort most recent first. + recentchoosermenu->set_show_tips(true); + recentchoosermenu->set_show_not_found(false); + recentchoosermenu->add_filter(recentfilter); + recentchoosermenu->signal_item_activated().connect( + sigc::bind(sigc::ptr_fun(&sp_recent_open), recentchoosermenu)); + + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_("Open _Recent"), true)); + menuitem->set_submenu(*recentchoosermenu); + menu->append(*menuitem); + continue; + } + + if (name == "separator") { + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::SeparatorMenuItem()); + menu->append(*menuitem); + continue; + } + + // Comments and items handled elsewhere. + if (name == "comment" || + name == "filters-list" || + name == "effects-list" ) { + continue; + } + + std::cerr << "build_menu: unhandled option: " << name << std::endl; + + } else { + std::cerr << "build_menu: xml node has no name!" << std::endl; + } + } +} + +Gtk::MenuBar* +build_menubar(Inkscape::UI::View::View* view) +{ + Gtk::MenuBar* menubar = Gtk::manage(new Gtk::MenuBar()); + build_menu(menubar, INKSCAPE.get_menus()->parent(), view); + SP_ACTIVE_DESKTOP->_menu_update.connect(sigc::ptr_fun(&set_menuitems)); + return menubar; +} + + +/* + 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 : diff --git a/src/ui/desktop/menubar.h b/src/ui/desktop/menubar.h new file mode 100644 index 0000000..065fb67 --- /dev/null +++ b/src/ui/desktop/menubar.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_DESKTOP_MENUBAR_H +#define SEEN_DESKTOP_MENUBAR_H + +/** + * @file + * Desktop main menu bar code. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +namespace Gtk { + class MenuBar; + class MenuItem; +} + +namespace Inkscape { +namespace UI { +namespace View { + class View; +} +} +} + +bool getStateFromPref(SPDesktop *dt, Glib::ustring item); +Gtk::MenuBar* build_menubar(Inkscape::UI::View::View* view); +Gtk::MenuItem *get_menu_item_for_verb(unsigned int verb, Inkscape::UI::View::View *); + +#endif // SEEN_DESKTOP_MENUBAR_H + +/* + 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 : diff --git a/src/ui/dialog-events.cpp b/src/ui/dialog-events.cpp new file mode 100644 index 0000000..17be2fe --- /dev/null +++ b/src/ui/dialog-events.cpp @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Event handler for dialog windows. + */ +/* Authors: + * bulia byak + * Johan Engelen + * + * Copyright (C) 2003-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "desktop.h" +#include "inkscape.h" +#include "include/macros.h" +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.h" + + +/** + * Remove focus from window to whoever it is transient for. + */ +void sp_dialog_defocus_cpp(Gtk::Window *win) +{ + //find out the document window we're transient for + Gtk::Window *w = win->get_transient_for(); + + //switch to it + if (w) { + w->present(); + } +} + +void +sp_dialog_defocus (GtkWindow *win) +{ + GtkWindow *w; + //find out the document window we're transient for + w = gtk_window_get_transient_for(GTK_WINDOW(win)); + //switch to it + + if (w) { + gtk_window_present (w); + } +} + + +/** + * Callback to defocus a widget's parent dialog. + */ +void sp_dialog_defocus_callback_cpp(Gtk::Entry *e) +{ + sp_dialog_defocus_cpp(dynamic_cast(e->get_toplevel())); +} + +void +sp_dialog_defocus_callback (GtkWindow * /*win*/, gpointer data) +{ + sp_dialog_defocus( GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(data))) ); +} + + + +void +sp_dialog_defocus_on_enter_cpp (Gtk::Entry *e) +{ + e->signal_activate().connect(sigc::bind(sigc::ptr_fun(&sp_dialog_defocus_callback_cpp), e)); +} + +void +sp_dialog_defocus_on_enter (GtkWidget *w) +{ + g_signal_connect ( G_OBJECT (w), "activate", + G_CALLBACK (sp_dialog_defocus_callback), w ); +} + + + +gboolean +sp_dialog_event_handler (GtkWindow *win, GdkEvent *event, gpointer data) +{ + +// if the focus is inside the Text and Font textview, do nothing + GObject *dlg = G_OBJECT(data); + if (g_object_get_data (dlg, "eatkeys")) { + return FALSE; + } + + gboolean ret = FALSE; + + switch (event->type) { + + case GDK_KEY_PRESS: + + switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: + sp_dialog_defocus (win); + ret = TRUE; + break; + case GDK_KEY_F4: + case GDK_KEY_w: + case GDK_KEY_W: + // close dialog + if (MOD__CTRL_ONLY(event)) { + + /* this code sends a delete_event to the dialog, + * instead of just destroying it, so that the + * dialog can do some housekeeping, such as remember + * its position. + */ + GdkEventAny event; + GtkWidget *widget = GTK_WIDGET(win); + event.type = GDK_DELETE; + event.window = gtk_widget_get_window (widget); + event.send_event = TRUE; + g_object_ref (G_OBJECT (event.window)); + gtk_main_do_event(reinterpret_cast(&event)); + g_object_unref (G_OBJECT (event.window)); + + ret = TRUE; + } + break; + default: // pass keypress to the canvas + break; + } + default: + ; + } + + return ret; + +} + + + +/** + * Make the argument dialog transient to the currently active document + * window. + */ +void sp_transientize(GtkWidget *dialog) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); +#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs + // _set_skip_taskbar_hint makes transient dialogs NON-transient! When dialogs + // are made transient (_set_transient_for), they are already removed from + // the taskbar in Win32. + if (prefs->getBool( "/options/dialogsskiptaskbar/value")) { + gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE); + } +#endif + + gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (transient_policy) { + + // if there's an active document window, attach dialog to it as a transient: + + if ( SP_ACTIVE_DESKTOP ) + { + SP_ACTIVE_DESKTOP->setWindowTransient (dialog, transient_policy); + } + } +} // end of sp_transientize() + +void on_transientize (SPDesktop *desktop, win_data *wd ) +{ + sp_transientize_callback (desktop, wd); +} + +void +sp_transientize_callback ( SPDesktop *desktop, win_data *wd ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited( "/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 1; +#endif + + if (!transient_policy) + return; + + if (wd->win) + { + desktop->setWindowTransient (wd->win, transient_policy); + } +} + +void on_dialog_hide (GtkWidget *w) +{ + if (w) + gtk_widget_hide (w); +} + +void on_dialog_unhide (GtkWidget *w) +{ + if (w) + gtk_widget_show (w); +} + +gboolean +sp_dialog_hide(GObject * /*object*/, gpointer data) +{ + GtkWidget *dlg = GTK_WIDGET(data); + + if (dlg) + gtk_widget_hide (dlg); + + return TRUE; +} + + + +gboolean +sp_dialog_unhide(GObject * /*object*/, gpointer data) +{ + GtkWidget *dlg = GTK_WIDGET(data); + + if (dlg) + gtk_widget_show (dlg); + + return TRUE; +} + + +/* + 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 : diff --git a/src/ui/dialog-events.h b/src/ui/dialog-events.h new file mode 100644 index 0000000..7bc7483 --- /dev/null +++ b/src/ui/dialog-events.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Event handler for dialog windows + */ +/* Authors: + * bulia byak + * + * Copyright (C) 2003-2014 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOG_EVENTS_H +#define SEEN_DIALOG_EVENTS_H + +#include + +/* + * event callback can only accept one argument, but we need two, + * hence this struct. + * each dialog has a local static copy: + * win is the dialog window + * stop is the transientize semaphore: when 0, retransientizing this dialog + * is allowed + */ + +namespace Gtk { +class Window; +class Entry; +} + +class SPDesktop; + +struct win_data { + GtkWidget *win; + guint stop; +}; + + +gboolean sp_dialog_event_handler ( GtkWindow *win, + GdkEvent *event, + gpointer data ); + +void sp_dialog_defocus_cpp (Gtk::Window *win); +void sp_dialog_defocus_callback_cpp(Gtk::Entry *e); +void sp_dialog_defocus_on_enter_cpp(Gtk::Entry *e); + +void sp_dialog_defocus ( GtkWindow *win ); +void sp_dialog_defocus_callback ( GtkWindow *win, gpointer data ); +void sp_dialog_defocus_on_enter ( GtkWidget *w ); +void sp_transientize ( GtkWidget *win ); + +void on_transientize ( SPDesktop *desktop, + win_data *wd ); + +void sp_transientize_callback ( SPDesktop *desktop, + win_data *wd ); + +void on_dialog_hide (GtkWidget *w); +void on_dialog_unhide (GtkWidget *w); + +//gboolean sp_dialog_hide (GObject *object, gpointer data); +//gboolean sp_dialog_unhide (GObject *object, gpointer data); + +#endif + +/* + 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 : diff --git a/src/ui/dialog/aboutbox.cpp b/src/ui/dialog/aboutbox.cpp new file mode 100644 index 0000000..72d31cc --- /dev/null +++ b/src/ui/dialog/aboutbox.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape About box - implementation. + */ +/* Authors: + * Derek P. Moore + * MenTaLguY + * Kees Cook + * Jon Phillips + * Abhishek Sharma + * + * Copyright (C) 2004 Derek P. Moore + * Copyright 2004 Kees Cook + * Copyright 2004 Jon Phillips + * Copyright 2005 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "aboutbox.h" + +#include + +#include +#include +#include + +#include +#include + +#include "document.h" +#include "inkscape-version.h" +#include "path-prefix.h" +#include "text-editing.h" + +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/view/svg-view-widget.h" + +#include "util/units.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static AboutBox *window=nullptr; + +void AboutBox::show_about() { + if (!window) + window = new AboutBox(); + window->show(); +} + +void AboutBox::hide_about() { + if (window) + window->hide(); +} + +/** + * Constructor + */ +AboutBox::AboutBox() + : _splash_widget(nullptr) +{ + // call this first + initStrings(); + + // Insert the Splash widget. This is placed directly into the + // content area of the dialog, whereas everything else is placed + // automatically by the Gtk::AboutDialog parent class + build_splash_widget(); + if (_splash_widget) { + get_content_area()->pack_end(*manage(_splash_widget), true, true); + _splash_widget->show_all(); + } + + // Set Application metadata, which will be automatically + // inserted into text widgets by the Gtk::AboutDialog parent class + // clang-format off + set_program_name ( "Inkscape"); + set_version ( Inkscape::version_string); + set_logo_icon_name( INKSCAPE_ICON("org.inkscape.Inkscape")); + set_website ( "https://www.inkscape.org"); + set_website_label (_("Inkscape website")); + set_license_type (Gtk::LICENSE_GPL_3_0); + set_copyright (_("© 2020 Inkscape Developers")); + set_comments (_("Open Source Scalable Vector Graphics Editor\n" + "Draw Freely.")); + // clang-format on + + get_content_area()->set_border_width(3); + get_action_area()->set_border_width(3); +} + +/** + * @brief Create a Gtk::AspectFrame containing the splash image + */ +void AboutBox::build_splash_widget() { + /* TRANSLATORS: This is the filename of the `About Inkscape' picture in + the `screens' directory. Thus the translation of "about.svg" should be + the filename of its translated version, e.g. about.zh.svg for Chinese. + + Please don't translate the filename unless the translated picture exists. */ + + // Try to get the translated version of the 'About Inkscape' file first. If the + // translation fails, or if the file does not exist, then fall-back to the + // default untranslated "about.svg" file + // + // FIXME? INKSCAPE_SCREENSDIR and "about.svg" are in UTF-8, not the + // native filename encoding... and the filename passed to sp_document_new + // should be in UTF-*8.. + auto about = Glib::build_filename(INKSCAPE_SCREENSDIR, _("about.svg")); + if (!Glib::file_test (about, Glib::FILE_TEST_EXISTS)) { + about = Glib::build_filename(INKSCAPE_SCREENSDIR, "about.svg"); + } + + // Create an Inkscape document from the 'About Inkscape' picture + SPDocument *doc=SPDocument::createNewDoc (about.c_str(), TRUE); + + // Leave _splash_widget as a nullptr if there is no document + if(doc) { + SPObject *version = doc->getObjectById("version"); + if ( version && SP_IS_TEXT(version) ) { + sp_te_set_repr_text_multiline (SP_TEXT (version), Inkscape::version_string); + } + doc->ensureUpToDate(); + + auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + + // temporary hack: halve the dimensions so the dialog will fit + double width=doc->getWidth().value("px") / 2.0; + double height=doc->getHeight().value("px") / 2.0; + viewer->setResize(width, height); + + _splash_widget = new Gtk::AspectFrame(); + _splash_widget->unset_label(); + _splash_widget->set_shadow_type(Gtk::SHADOW_NONE); + _splash_widget->property_ratio() = width / height; + _splash_widget->add(*viewer); + } +} + +/** + * @brief Read the author and translator credits from file + */ +void AboutBox::initStrings() { + //############################## + //# A U T H O R S + //############################## + + // Create an empty vector to store the list of authors + std::vector authors; + + // Try to copy the list of authors from the "AUTHORS" file, which + // should have been installed into the share/doc directory + auto authors_filename = Glib::build_filename(INKSCAPE_DOCDIR, "AUTHORS"); + std::ifstream authors_filestream(authors_filename); + if(authors_filestream) { + std::string author_line; + + while (std::getline(authors_filestream, author_line)) { + authors.emplace_back(author_line); + } + } + + // Set the author credits in this dialog, using the author list + set_authors(authors); + + //############################## + //# T R A N S L A T O R S + //############################## + + Glib::ustring translators_text; + + // TRANSLATORS: Put here your name (and other national contributors') + // one per line in the form of: name surname (email). Use \n for newline. + Glib::ustring thisTranslation = _("translator-credits"); + + /** + * See if the translators for the current language + * made an entry for "translator-credits". If it exists, + * put it at the top of the window, add some space between + * it and the list of all translators. + * + * NOTE: Do we need 2 more .po entries for titles: + * "translators for this language" + * "all translators" ?? + */ + if (thisTranslation != "translator-credits") { + translators_text.append(thisTranslation); + translators_text.append("\n\n\n"); + } + + auto translators_filename = Glib::build_filename(INKSCAPE_DOCDIR, "TRANSLATORS"); + + if (Glib::file_test (translators_filename, Glib::FILE_TEST_EXISTS)) { + auto all_translators = Glib::file_get_contents(translators_filename); + translators_text.append(all_translators); + } + + set_translator_credits(translators_text); +} + +void AboutBox::on_response(int response_id) { + hide(); +} +} // 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 : diff --git a/src/ui/dialog/aboutbox.h b/src/ui/dialog/aboutbox.h new file mode 100644 index 0000000..e5b9f27 --- /dev/null +++ b/src/ui/dialog/aboutbox.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape About box + * + * The standard Gnome::UI::About class doesn't include a place to stuff + * a renderable View that holds the classic Inkscape "about.svg". + */ +/* Author: + * Kees Cook + * + * Copyright (C) 2005 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ABOUTBOX_H +#define INKSCAPE_UI_DIALOG_ABOUTBOX_H + +#include + +namespace Gtk { +class AspectFrame; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class AboutBox : public Gtk::AboutDialog { + +public: + + static void show_about(); + static void hide_about(); + +private: + + AboutBox(); + + /** A widget containing an SVG "splash screen" + * image to display in the content area of the dialo + */ + Gtk::AspectFrame *_splash_widget; + + void initStrings(); + void build_splash_widget(); + + void on_response(int response_id) override; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_ABOUTBOX_H + +/* + 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 : diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp new file mode 100644 index 0000000..31605a0 --- /dev/null +++ b/src/ui/dialog/align-and-distribute.cpp @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Align and Distribute dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington + * Aubanel MONNIER + * Frank Felfe + * Lauris Kaplinski + * Tim Dwyer + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include <2geom/transforms.h> + +#include + +#include "align-and-distribute.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "graphlayout.h" +#include "inkscape.h" +#include "preferences.h" +#include "removeoverlap.h" +#include "text-editing.h" +#include "unclump.h" +#include "verbs.h" + +#include "object/sp-flowtext.h" +#include "object/sp-item-transform.h" +#include "object/sp-root.h" +#include "object/sp-text.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/spinbutton.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/////////helper classes////////////////////////////////// + +Action::Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog): + _dialog(dialog), + _id(std::move(id)), + _parent(parent) +{ + Gtk::Image* pIcon = Gtk::manage(new Gtk::Image()); + pIcon = sp_get_icon_image(_id, Gtk::ICON_SIZE_LARGE_TOOLBAR); + Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + + pButton->signal_clicked() + .connect(sigc::mem_fun(*this, &Action::on_button_click)); + pButton->set_tooltip_text(tiptext); + parent.attach(*pButton, column, row, 1, 1); +} + + +void ActionAlign::do_node_action(Inkscape::UI::Tools::NodeTool *nt, int verb) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prev_pref = prefs->getInt("/dialogs/align/align-nodes-to"); + switch(verb){ + case SP_VERB_ALIGN_HORIZONTAL_LEFT: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_CENTER: + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_RIGHT: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_VERTICAL_TOP: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_CENTER: + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_BOTTOM: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_BOTH_CENTER: + nt->_multipath->alignNodes(Geom::X); + nt->_multipath->alignNodes(Geom::Y); + break; + default:return; + } + prefs->setInt("/dialogs/align/align-nodes-to", prev_pref ); +} + +void ActionAlign::do_action(SPDesktop *desktop, int index) +{ + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); + + std::vector selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + Coeffs a = _allCoeffs[index]; // copy + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + Selection::CompareSize horiz = (a.mx0 != 0.0) || (a.mx1 != 0.0) + ? Selection::VERTICAL : Selection::HORIZONTAL; + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(horiz); + break; + case SMALLEST: + focus = selection->smallestItem(horiz); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) + b = focus->desktopPreferredBounds(); + + g_return_if_fail(b); + + if (desktop->is_yaxisdown()) { + std::swap(a.my0, a.my1); + std::swap(a.sy0, a.sy1); + } + + // Generate the move point from the selected bounding box + Geom::Point mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X], + a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]); + + if (sel_as_group) { + if (focus) { + // use bounding box of all selected elements except the "focused" element + Inkscape::ObjectSet copy; + copy.add(selection->objects().begin(), selection->objects().end()); + copy.remove(focus); + b = copy.preferredBounds(); + } else { + // use bounding box of all selected elements + b = selection->preferredBounds(); + } + } + + //Move each item in the selected list separately + bool changed = false; + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + if (!sel_as_group) + b = (item)->desktopPreferredBounds(); + if (b && (!focus || (item) != focus)) { + Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X], + a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]); + Geom::Point const mp_rel( mp - sp ); + if (LInfty(mp_rel) > 1e-9) { + item->move_rel(Geom::Translate(mp_rel)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done( desktop->getDocument() , SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align")); + } +} + + +ActionAlign::Coeffs const ActionAlign::_allCoeffs[19] = { + {1., 0., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR}, + {1., 0., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT}, + {.5, .5, 0., 0., .5, .5, 0., 0., SP_VERB_ALIGN_HORIZONTAL_CENTER}, + {0., 1., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT}, + {0., 1., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP}, + {0., 0., .5, .5, 0., 0., .5, .5, SP_VERB_ALIGN_VERTICAL_CENTER}, + {0., 0., 1., 0., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM}, + {0., 0., 1., 0., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR}, + {1., 0., 0., 1., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT}, + {0., 1., 0., 1., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT}, + {0., 1., 1., 0., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT}, + {1., 0., 1., 0., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT}, + {0., 1., 1., 0., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR}, + {1., 0., 1., 0., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR}, + {1., 0., 0., 1., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR}, + {0., 1., 0., 1., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR}, + {.5, .5, .5, .5, .5, .5, .5, .5, SP_VERB_ALIGN_BOTH_CENTER} +}; + +void ActionAlign::do_verb_action(SPDesktop *desktop, int verb) +{ + Inkscape::UI::Tools::ToolBase *event_context = desktop->getEventContext(); + if (INK_IS_NODE_TOOL(event_context)) { + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + if(!nt->_selected_nodes->empty()){ + do_node_action(nt, verb); + return; + } + } + do_action(desktop, verb_to_coeff(verb)); +} + +int ActionAlign::verb_to_coeff(int verb) { + + for(guint i = 0; i < G_N_ELEMENTS(_allCoeffs); i++) { + if (_allCoeffs[i].verb_id == verb) { + return i; + } + } + + return -1; +} + +BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd) : + item(pItem), + bbox (bounds) +{ + anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation]; +} +BBoxSort::BBoxSort(const BBoxSort &rhs) + //NOTE : this copy ctor is called O(sort) when sorting the vector + //this is bad. The vector should be a vector of pointers. + //But I'll wait the bohem GC before doing that += default; + +bool operator< (const BBoxSort &a, const BBoxSort &b) +{ + return (a.anchor < b.anchor); +} + +class ActionDistribute : public Action { +public : + ActionDistribute(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + bool onInterSpace, + Geom::Dim2 orientation, + double kBegin, double kEnd + ): + Action(id, tiptext, row, column, + dialog.distribute_table(), dialog), + _dialog(dialog), + _onInterSpace(onInterSpace), + _orientation(orientation), + _kBegin(kBegin), + _kEnd( kEnd) + {} + +private : + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + std::vector::iterator second(selected.begin()); + ++second; + if (second == selected.end()) return; + + double kBegin = _kBegin; + double kEnd = _kEnd; + if (_orientation == Geom::Y && desktop->is_yaxisdown()) { + kBegin = 1. - kBegin; + kEnd = 1. - kEnd; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + std::vector< BBoxSort > sorted; + for (auto item : selected){ + Geom::OptRect bbox = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, _orientation, kBegin, kEnd); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + unsigned int len = sorted.size(); + bool changed = false; + if (_onInterSpace) + { + //overall bboxes span + float dist = (sorted.back().bbox.max()[_orientation] - + sorted.front().bbox.min()[_orientation]); + //space eaten by bboxes + float span = 0; + for (unsigned int i = 0; i < len; i++) + { + span += sorted[i].bbox[_orientation].extent(); + } + //new distance between each bbox + float step = (dist - span) / (len - 1); + float pos = sorted.front().bbox.min()[_orientation]; + for ( std::vector ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + if (!Geom::are_near(pos, it->bbox.min()[_orientation], 1e-6)) { + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it->bbox.min()[_orientation]; + it->item->move_rel(Geom::Translate(t)); + changed = true; + } + pos += it->bbox[_orientation].extent(); + pos += step; + } + } + else + { + //overall anchor span + float dist = sorted.back().anchor - sorted.front().anchor; + //distance between anchors + float step = dist / (len - 1); + + for ( unsigned int i = 0; i < len ; i ++ ) + { + BBoxSort & it(sorted[i]); + //new anchor position + float pos = sorted.front().anchor + i * step; + //Don't move if we are really close + if (!Geom::are_near(pos, it.anchor, 1e-6)) { + //Compute translation + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it.anchor; + //translate + it.item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + if (changed) { + DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute")); + } + } + guint _index; + AlignAndDistribute &_dialog; + bool _onInterSpace; + Geom::Dim2 _orientation; + + double _kBegin; + double _kEnd; + +}; + + +class ActionNode : public Action { +public : + ActionNode(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint column, + AlignAndDistribute &dialog, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, 0, column, + dialog.nodes_table(), dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + + void on_button_click() override { + if (!_dialog.getDesktop()) { + return; + } + + Inkscape::UI::Tools::ToolBase *event_context = _dialog.getDesktop()->getEventContext(); + + if (!INK_IS_NODE_TOOL(event_context)) { + return; + } + + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + + if (_distribute) { + nt->_multipath->distributeNodes(_orientation); + } else { + nt->_multipath->alignNodes(_orientation); + } + } +}; + +class ActionRemoveOverlaps : public Action { +private: + Gtk::Label removeOverlapXGapLabel; + Gtk::Label removeOverlapYGapLabel; + Inkscape::UI::Widget::SpinButton removeOverlapXGap; + Inkscape::UI::Widget::SpinButton removeOverlapYGap; + +public: + ActionRemoveOverlaps(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column + 4, + dialog.removeOverlap_table(), dialog) + { + dialog.removeOverlap_table().set_column_spacing(3); + + removeOverlapXGap.set_digits(1); + removeOverlapXGap.set_size_request(60, -1); + removeOverlapXGap.set_increments(1.0, 0); + removeOverlapXGap.set_range(-1000.0, 1000.0); + removeOverlapXGap.set_value(0); + removeOverlapXGap.set_tooltip_text(_("Minimum horizontal gap (in px units) between bounding boxes")); + //TRANSLATORS: "H:" stands for horizontal gap + removeOverlapXGapLabel.set_text_with_mnemonic(C_("Gap", "_H:")); + removeOverlapXGapLabel.set_mnemonic_widget(removeOverlapXGap); + + removeOverlapYGap.set_digits(1); + removeOverlapYGap.set_size_request(60, -1); + removeOverlapYGap.set_increments(1.0, 0); + removeOverlapYGap.set_range(-1000.0, 1000.0); + removeOverlapYGap.set_value(0); + removeOverlapYGap.set_tooltip_text(_("Minimum vertical gap (in px units) between bounding boxes")); + /* TRANSLATORS: Vertical gap */ + removeOverlapYGapLabel.set_text_with_mnemonic(C_("Gap", "_V:")); + removeOverlapYGapLabel.set_mnemonic_widget(removeOverlapYGap); + + dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, row, 1, 1); + } + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // xGap and yGap are the minimum space required between bounding rectangles. + double const xGap = removeOverlapXGap.get_value(); + double const yGap = removeOverlapYGap.get_value(); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector vec(tmp.begin(), tmp.end()); + removeoverlap(vec, xGap, yGap); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Remove overlaps")); + } +}; + +class ActionGraphLayout : public Action { +public: + ActionGraphLayout(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector vec(tmp.begin(), tmp.end()); + graphlayout(vec); + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Arrange connector network")); + } +}; + +class ActionExchangePositions : public Action { +public: + enum SortOrder { + None, + ZOrder, + Clockwise + }; + + ActionExchangePositions(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, SortOrder order = None) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog), + sortOrder(order) + {}; + + +private : + const SortOrder sortOrder; + static boost::optional center; + + static bool sort_compare(const SPItem * a,const SPItem * b) { + if (a == nullptr) return false; + if (b == nullptr) return true; + if (center) { + Geom::Point point_a = a->getCenter() - (*center); + Geom::Point point_b = b->getCenter() - (*center); + // First criteria: Sort according to the angle to the center point + double angle_a = atan2(double(point_a[Geom::Y]), double(point_a[Geom::X])); + double angle_b = atan2(double(point_b[Geom::Y]), double(point_b[Geom::X])); + double dt_yaxisdir = SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->yaxisdir() : 1; + angle_a *= -dt_yaxisdir; + angle_b *= -dt_yaxisdir; + if (angle_a != angle_b) return (angle_a < angle_b); + // Second criteria: Sort according to the distance the center point + Geom::Coord length_a = point_a.length(); + Geom::Coord length_b = point_b.length(); + if (length_a != length_b) return (length_a > length_b); + } + // Last criteria: Sort according to the z-coordinate + return sp_item_repr_compare_position(a,b)<0; + } + + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // sort the list + if (sortOrder != None) { + if (sortOrder == Clockwise) { + center = selection->center(); + } else { // sorting by ZOrder is outomatically done by not setting the center + center.reset(); + } + sort(selected.begin(),selected.end(),sort_compare); + } + + Geom::Point p1 = selected.back()->getCenter(); + for (SPItem *item : selected) + { + Geom::Point p2 = item->getCenter(); + Geom::Point delta = p1 - p2; + item->move_rel(Geom::Translate(delta[Geom::X],delta[Geom::Y] )); + p1 = p2; + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Exchange Positions")); + } +}; + +// instantiate the private static member +boost::optional ActionExchangePositions::center; + +class ActionUnclump : public Action { +public : + ActionUnclump(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector x(tmp.begin(), tmp.end()); + unclump (x); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Unclump")); + } +}; + +class ActionRandomize : public Action { +public : + ActionRandomize(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + Geom::OptRect sel_bbox = !prefs_bbox ? selection->visualBounds() : selection->geometricBounds(); + if (!sel_bbox) { + return; + } + + // This bbox is cached between calls to randomize, so that there's no growth nor shrink + // nor drift on sequential randomizations. Discard cache on global (or better active + // desktop's) selection_change signal. + if (!_dialog.randomize_bbox) { + _dialog.randomize_bbox = *sel_bbox; + } + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + Geom::OptRect item_box = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (item_box) { + // find new center, staying within bbox + double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent()); + double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent()); + // displacement is the new center minus old: + Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min()); + item->move_rel(Geom::Translate(t)); + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Randomize positions")); + } +}; + +struct Baselines +{ + SPItem *_item; + Geom::Point _base; + Geom::Dim2 _orientation; + Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) : + _item (item), + _base (base), + _orientation (orientation) + {} +}; + +static bool operator< (const Baselines &a, const Baselines &b) +{ + return (a._base[a._orientation] < b._base[b._orientation]); +} + +class ActionBaseline : public Action { +public : + ActionBaseline(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, + Gtk::Grid &table, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, row, column, + table, dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL); + Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL); + + std::vector sorted; + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X]; + if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y]; + if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X]; + if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y]; + Baselines b (item, base, _orientation); + sorted.push_back(b); + } + } + } + + if (sorted.size() <= 1) return; + + //sort baselines + std::stable_sort(sorted.begin(), sorted.end()); + + bool changed = false; + + if (_distribute) { + double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1); + for (unsigned int i = 0; i < sorted.size(); i++) { + SPItem *item = sorted[i]._item; + Geom::Point base = sorted[i]._base; + Geom::Point t(0.0, 0.0); + t[_orientation] = b_min[_orientation] + step * i - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute text baselines")); + } + + } else { //align + Geom::Point ref_point; + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(Selection::AREA); + break; + case SMALLEST: + focus = selection->smallestItem(Selection::AREA); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) { + if (SP_IS_TEXT (focus) || SP_IS_FLOWTEXT (focus)) { + ref_point = *(te_get_layout(focus)->baselineAnchorPoint())*(focus->i2dt_affine()); + } else { + ref_point = focus->desktopPreferredBounds()->min(); + } + } else { + ref_point = b->min(); + } + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + Geom::Point t(0.0, 0.0); + t[_orientation] = ref_point[_orientation] - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align text baselines")); + } + } + } +}; + + + +static void on_tool_changed(AlignAndDistribute *daad) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && desktop->getEventContext()) + daad->setMode(tools_active(desktop) == TOOLS_NODES); + else + daad->setMode(false); + +} + +static void on_selection_changed(AlignAndDistribute *daad) +{ + daad->randomize_bbox = Geom::OptRect(); +} + +///////////////////////////////////////////////////////// + + + + +AlignAndDistribute::AlignAndDistribute() + : UI::Widget::Panel("/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE), + randomize_bbox(), + _alignFrame(_("Align")), + _distributeFrame(_("Distribute")), + _rearrangeFrame(_("Rearrange")), + _removeOverlapFrame(_("Remove overlaps")), + _nodesFrame(_("Nodes")), + _alignTable(), + _distributeTable(), + _rearrangeTable(), + _removeOverlapTable(), + _nodesTable(), + _anchorLabel(_("Relative to: ")), + _anchorLabelNode(_("Relative to: ")) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + //Instantiate the align buttons + addAlignButton(INKSCAPE_ICON("align-horizontal-right-to-anchor"), + _("Align right edges of objects to the left edge of the anchor"), + 0, 0); + addAlignButton(INKSCAPE_ICON("align-horizontal-left"), + _("Align left edges"), + 0, 1); + addAlignButton(INKSCAPE_ICON("align-horizontal-center"), + _("Center on vertical axis"), + 0, 2); + addAlignButton(INKSCAPE_ICON("align-horizontal-right"), + _("Align right sides"), + 0, 3); + addAlignButton(INKSCAPE_ICON("align-horizontal-left-to-anchor"), + _("Align left edges of objects to the right edge of the anchor"), + 0, 4); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom-to-anchor"), + _("Align bottom edges of objects to the top edge of the anchor"), + 1, 0); + addAlignButton(INKSCAPE_ICON("align-vertical-top"), + _("Align top edges"), + 1, 1); + addAlignButton(INKSCAPE_ICON("align-vertical-center"), + _("Center on horizontal axis"), + 1, 2); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom"), + _("Align bottom edges"), + 1, 3); + addAlignButton(INKSCAPE_ICON("align-vertical-top-to-anchor"), + _("Align top edges of objects to the bottom edge of the anchor"), + 1, 4); + + //Baseline aligns + addBaselineButton(INKSCAPE_ICON("align-horizontal-baseline"), + _("Align baseline anchors of texts horizontally"), + 0, 5, this->align_table(), Geom::X, false); + addBaselineButton(INKSCAPE_ICON("align-vertical-baseline"), + _("Align baselines of texts"), + 1, 5, this->align_table(), Geom::Y, false); + + //The distribute buttons + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-gaps"), + _("Make horizontal gaps between objects equal"), + 0, 4, true, Geom::X, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-left"), + _("Distribute left edges equidistantly"), + 0, 1, false, Geom::X, 1., 0.); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-center"), + _("Distribute centers equidistantly horizontally"), + 0, 2, false, Geom::X, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-right"), + _("Distribute right edges equidistantly"), + 0, 3, false, Geom::X, 0., 1.); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-gaps"), + _("Make vertical gaps between objects equal"), + 1, 4, true, Geom::Y, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-top"), + _("Distribute top edges equidistantly"), + 1, 1, false, Geom::Y, 0, 1); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-center"), + _("Distribute centers equidistantly vertically"), + 1, 2, false, Geom::Y, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-bottom"), + _("Distribute bottom edges equidistantly"), + 1, 3, false, Geom::Y, 1., 0.); + + //Baseline distribs + addBaselineButton(INKSCAPE_ICON("distribute-horizontal-baseline"), + _("Distribute baseline anchors of texts horizontally"), + 0, 5, this->distribute_table(), Geom::X, true); + addBaselineButton(INKSCAPE_ICON("distribute-vertical-baseline"), + _("Distribute baselines of texts vertically"), + 1, 5, this->distribute_table(), Geom::Y, true); + + // Rearrange + //Graph Layout + addGraphLayoutButton(INKSCAPE_ICON("distribute-graph"), + _("Nicely arrange selected connector network"), + 0, 0); + addExchangePositionsButton(INKSCAPE_ICON("exchange-positions"), + _("Exchange positions of selected objects - selection order"), + 0, 1); + addExchangePositionsByZOrderButton(INKSCAPE_ICON("exchange-positions-zorder"), + _("Exchange positions of selected objects - stacking order"), + 0, 2); + addExchangePositionsClockwiseButton(INKSCAPE_ICON("exchange-positions-clockwise"), + _("Exchange positions of selected objects - clockwise rotate"), + 0, 3); + + //Randomize & Unclump + addRandomizeButton(INKSCAPE_ICON("distribute-randomize"), + _("Randomize centers in both dimensions"), + 0, 4); + addUnclumpButton(INKSCAPE_ICON("distribute-unclump"), + _("Unclump objects: try to equalize edge-to-edge distances"), + 0, 5); + + //Remove overlaps + addRemoveOverlapsButton(INKSCAPE_ICON("distribute-remove-overlaps"), + _("Move objects as little as possible so that their bounding boxes do not overlap"), + 0, 0); + + //Node Mode buttons + // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common + // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means. + // There is no doubt some ambiguity. For this reason the descriptions are different. + addNodeButton(INKSCAPE_ICON("align-vertical-node"), + _("Align selected nodes to a common horizontal line"), + 0, Geom::X, false); + addNodeButton(INKSCAPE_ICON("align-horizontal-node"), + _("Align selected nodes to a common vertical line"), + 1, Geom::Y, false); + addNodeButton(INKSCAPE_ICON("distribute-horizontal-node"), + _("Distribute selected nodes horizontally"), + 2, Geom::X, true); + addNodeButton(INKSCAPE_ICON("distribute-vertical-node"), + _("Distribute selected nodes vertically"), + 3, Geom::Y, true); + + //Rest of the widgetry + + _combo.append(_("Last selected")); + _combo.append(_("First selected")); + _combo.append(_("Biggest object")); + _combo.append(_("Smallest object")); + _combo.append(_("Page")); + _combo.append(_("Drawing")); + _combo.append(_("Selection Area")); + _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6)); + _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change)); + + _comboNode.append(_("Last selected")); + _comboNode.append(_("First selected")); + _comboNode.append(_("Middle of selection")); + _comboNode.append(_("Min value")); + _comboNode.append(_("Max value")); + _comboNode.set_active(prefs->getInt("/dialogs/align/align-nodes-to", 2)); + _comboNode.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_node_ref_change)); + + Gtk::Image* selgrp_icon = Gtk::manage(new Gtk::Image()); + selgrp_icon = sp_get_icon_image("align-sel-as-group", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _selgrp.add(*selgrp_icon); + + _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups")); + _selgrp.set_relief(Gtk::RELIEF_NONE); + _selgrp.set_tooltip_text(_("Treat selection as group")); + _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled)); + _anchorBox.pack_end(_selgrp, false, false); + _anchorBox.pack_end(_combo, false, false); + _anchorBox.pack_end(_anchorLabel, false, false); + + _anchorBoxNode.pack_end(_comboNode, false, false); + _anchorBoxNode.pack_end(_anchorLabelNode, false, false); + + Gtk::Image* oncanvas_icon = Gtk::manage(new Gtk::Image()); + oncanvas_icon = sp_get_icon_image("align-on-canvas", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _oncanvas.add(*oncanvas_icon); + + _oncanvas.set_relief(Gtk::RELIEF_NONE); + _oncanvas.set_tooltip_text(_("Enable on-canvas alignment handles.")); + _anchorBox.pack_start(_oncanvas, false, false); + _oncanvas.set_active(prefs->getBool("/dialogs/align/oncanvas")); + _oncanvas.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_oncanvas_toggled)); + + // Right align the buttons + _alignTableBox.pack_end(_alignTable, false, false); + _distributeTableBox.pack_end(_distributeTable, false, false); + _rearrangeTableBox.pack_end(_rearrangeTable, false, false); + _removeOverlapTableBox.pack_end(_removeOverlapTable, false, false); + _nodesTableBox.pack_end(_nodesTable, false, false); + + _alignBox.pack_start(_anchorBox); + _alignBox.pack_start(_selgrpBox); + _alignBox.pack_start(_alignTableBox); + + _alignBoxNode.pack_start(_anchorBoxNode, false, false); + _alignBoxNode.pack_start(_nodesTableBox); + + + _alignFrame.add(_alignBox); + _distributeFrame.add(_distributeTableBox); + _rearrangeFrame.add(_rearrangeTableBox); + _removeOverlapFrame.add(_removeOverlapTableBox); + _nodesFrame.add(_alignBoxNode); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(4); + + // Notebook for individual transformations + + contents->pack_start(_alignFrame, true, true); + contents->pack_start(_distributeFrame, true, true); + contents->pack_start(_rearrangeFrame, true, true); + contents->pack_start(_removeOverlapFrame, true, true); + contents->pack_start(_nodesFrame, true, false); + + //Connect to the global tool change signal + _toolChangeConn = INKSCAPE.signal_eventcontext_set.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_tool_changed), this))); + + // Connect to the global selection change, to invalidate cached randomize_bbox + _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_selection_changed), this))); + randomize_bbox = Geom::OptRect(); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &AlignAndDistribute::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + on_tool_changed (this); // set current mode +} + +AlignAndDistribute::~AlignAndDistribute() +{ + for (auto & it : _actionList) { + delete it; + } + + _toolChangeConn.disconnect(); + _selChangeConn.disconnect(); + _desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +void AlignAndDistribute::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + on_tool_changed (this); + } +} + + +void AlignAndDistribute::on_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_node_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-nodes-to", _comboNode.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_selgrp_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::on_oncanvas_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/oncanvas", _oncanvas.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::setMode(bool nodeEdit) +{ + //Act on widgets used in node mode + void ( Gtk::Widget::*mNode) () = nodeEdit ? + &Gtk::Widget::show_all : &Gtk::Widget::hide; + + //Act on widgets used in selection mode + void ( Gtk::Widget::*mSel) () = nodeEdit ? + &Gtk::Widget::hide : &Gtk::Widget::show_all; + + ((_alignFrame).*(mSel))(); + ((_distributeFrame).*(mSel))(); + ((_rearrangeFrame).*(mSel))(); + ((_removeOverlapFrame).*(mSel))(); + ((_nodesFrame).*(mNode))(); + _getContents()->queue_resize(); + +} +void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionAlign( + id, tiptext, row, col, + *this , col + row * 5)); +} +void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd) +{ + _actionList.push_back( + new ActionDistribute( + id, tiptext, row, col, *this , + onInterSpace, orientation, + kBegin, kEnd + ) + ); +} + +void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionNode( + id, tiptext, col, + *this, orientation, distribute)); +} + +void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRemoveOverlaps( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionGraphLayout( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsByZOrderButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::ZOrder) + ); +} + +void AlignAndDistribute::addExchangePositionsClockwiseButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::Clockwise) + ); +} + +void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionUnclump( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRandomize( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionBaseline( + id, tiptext, row, col, + *this, table, orientation, distribute)); +} + +} // 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 : diff --git a/src/ui/dialog/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h new file mode 100644 index 0000000..175ebdc --- /dev/null +++ b/src/ui/dialog/align-and-distribute.h @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Align and Distribute dialog + */ +/* Authors: + * Bryce W. Harrington + * Aubanel MONNIER + * Frank Felfe + * Lauris Kaplinski + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H +#define INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +#include +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include +#include +#include +#include +#include + +#include "2geom/rect.h" +#include "ui/dialog/desktop-tracker.h" + +class SPItem; + +namespace Inkscape { +namespace UI { +namespace Tools{ +class NodeTool; +} +namespace Dialog { + +class Action; + + +class AlignAndDistribute : public Widget::Panel { +public: + AlignAndDistribute(); + ~AlignAndDistribute() override; + + static AlignAndDistribute &getInstance() { return *new AlignAndDistribute(); } + + Gtk::Grid &align_table(){return _alignTable;} + Gtk::Grid &distribute_table(){return _distributeTable;} + Gtk::Grid &rearrange_table(){return _rearrangeTable;} + Gtk::Grid &removeOverlap_table(){return _removeOverlapTable;} + Gtk::Grid &nodes_table(){return _nodesTable;} + + void setMode(bool nodeEdit); + + Geom::OptRect randomize_bbox; + +protected: + + void on_ref_change(); + void on_node_ref_change(); + void on_selgrp_toggled(); + void on_oncanvas_toggled(); + void addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd); + void addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute); + void addRemoveOverlapsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addGraphLayoutButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsByZOrderButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsClockwiseButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute); + void setTargetDesktop(SPDesktop *desktop); + + std::list _actionList; + UI::Widget::Frame _alignFrame, _distributeFrame, _rearrangeFrame, _removeOverlapFrame, _nodesFrame; + Gtk::Grid _alignTable, _distributeTable, _rearrangeTable, _removeOverlapTable, _nodesTable; + Gtk::HBox _anchorBox; + Gtk::HBox _selgrpBox; + Gtk::VBox _alignBox; + Gtk::VBox _alignBoxNode; + Gtk::HBox _alignTableBox; + Gtk::HBox _distributeTableBox; + Gtk::HBox _rearrangeTableBox; + Gtk::HBox _removeOverlapTableBox; + Gtk::HBox _nodesTableBox; + Gtk::Label _anchorLabel; + Gtk::Label _anchorLabelNode; + Gtk::ToggleButton _selgrp; + Gtk::ToggleButton _oncanvas; + Gtk::ComboBoxText _combo; + Gtk::HBox _anchorBoxNode; + Gtk::ComboBoxText _comboNode; + + SPDesktop *_desktop; + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + sigc::connection _toolChangeConn; + sigc::connection _selChangeConn; +private: + AlignAndDistribute(AlignAndDistribute const &d) = delete; + AlignAndDistribute& operator=(AlignAndDistribute const &d) = delete; +}; + + +struct BBoxSort +{ + SPItem *item; + float anchor; + Geom::Rect bbox; + BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd); + BBoxSort(const BBoxSort &rhs); +}; +bool operator< (const BBoxSort &a, const BBoxSort &b); + + +class Action { +public : + + enum AlignTarget { LAST=0, FIRST, BIGGEST, SMALLEST, PAGE, DRAWING, SELECTION }; + enum AlignTargetNode { LAST_NODE=0, FIRST_NODE, MID_NODE, MIN_NODE, MAX_NODE }; + Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog); + + virtual ~Action()= default; + + AlignAndDistribute &_dialog; + +private : + virtual void on_button_click(){} + + Glib::ustring _id; + Gtk::Grid &_parent; +}; + + +class ActionAlign : public Action { +public : + struct Coeffs { + double mx0, mx1, my0, my1; + double sx0, sx1, sy0, sy1; + int verb_id; + }; + ActionAlign(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + guint coeffIndex): + Action(id, tiptext, row, column, + dialog.align_table(), dialog), + _index(coeffIndex), + _dialog(dialog) + {} + + /* + * Static function called to align from a keyboard shortcut + */ + static void do_verb_action(SPDesktop *desktop, int verb); + static int verb_to_coeff(int verb); + +private : + + + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + do_action(desktop, _index); + } + + static void do_action(SPDesktop *desktop, int index); + static void do_node_action(Inkscape::UI::Tools::NodeTool *nt, int index); + + guint _index; + AlignAndDistribute &_dialog; + + static const Coeffs _allCoeffs[19]; + +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +/* + 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 : diff --git a/src/ui/dialog/arrange-tab.h b/src/ui/dialog/arrange-tab.h new file mode 100644 index 0000000..42e141e --- /dev/null +++ b/src/ui/dialog/arrange-tab.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arrange tools base class + */ +/* Authors: + * * Declara Denis + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_ARRANGE_TAB_H + +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * This interface should be implemented by each arrange mode. + * The class is a Gtk::VBox and will be displayed as a tab in + * the dialog + */ +class ArrangeTab : public Gtk::VBox +{ +public: + ArrangeTab() = default;; + ~ArrangeTab() override = default;; + + /** + * Do the actual work! This method is invoked to actually arrange the + * selection + */ + virtual void arrange() = 0; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +#endif /* INKSCAPE_UI_DIALOG_ARRANGE_TAB_H */ + +/* + 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 : diff --git a/src/ui/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp new file mode 100644 index 0000000..175c2ad --- /dev/null +++ b/src/ui/dialog/attrdialog.cpp @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#include "attrdialog.h" + +#include "verbs.h" +#include "selection.h" +#include "document-undo.h" +#include "message-context.h" +#include "message-stack.h" +#include "style.h" +#include "ui/icon-loader.h" +#include "ui/widget/iconrenderer.h" + +#include "xml/node-event-vector.h" +#include "xml/attribute-record.h" + +#include +#include + +static void on_attr_changed (Inkscape::XML::Node * repr, + const gchar * name, + const gchar * /*old_value*/, + const gchar * new_value, + bool /*is_interactive*/, + gpointer data) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, name, new_value); +} + +static void on_content_changed (Inkscape::XML::Node * repr, + gchar const * oldcontent, + gchar const * newcontent, + gpointer data) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, "content", repr->content()); +} + +Inkscape::XML::NodeEventVector _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_attr_changed, + on_content_changed, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog); +/** + * Constructor + * A treeview whose each row corresponds to an XML attribute of a selected node + * New attribute can be added by clicking '+' at bottom of the attr pane. '-' + */ +AttrDialog::AttrDialog() + : UI::Widget::Panel("/dialogs/attr", SP_VERB_DIALOG_ATTR) + , _desktop(nullptr) + , _repr(nullptr) +{ + set_size_request(20, 15); + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + _treeView.set_headers_visible(true); + _treeView.set_hover_selection(true); + _treeView.set_activate_on_single_click(true); + _treeView.set_can_focus(false); + _scrolledWindow.add(_treeView); + _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + _store = Gtk::ListStore::create(_attrColumns); + _treeView.set_model(_store); + + Inkscape::UI::Widget::IconRenderer * addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + + _treeView.append_column("", *addRenderer); + Gtk::TreeViewColumn *col = _treeView.get_column(0); + if (col) { + auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + col->set_clickable(true); + col->set_widget(*add_icon); + add_icon->set_tooltip_text(_("Add a new attribute")); + add_icon->show(); + auto button = add_icon->get_parent()->get_parent()->get_parent(); + // Assign the button event so that create happens BEFORE delete. If this code + // isn't in this exact way, the onAttrDelete is called when the header lines are pressed. + button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false); + } + addRenderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete)); + _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed)); + _treeView.set_search_column(-1); + + _nameRenderer = Gtk::manage(new Gtk::CellRendererText()); + _nameRenderer->property_editable() = true; + _nameRenderer->property_placeholder_text().set_value(_("Attribute Name")); + _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited)); + _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit)); + _treeView.append_column(_("Name"), *_nameRenderer); + _nameCol = _treeView.get_column(1); + if (_nameCol) { + _nameCol->set_resizable(true); + _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName); + } + status.set_halign(Gtk::ALIGN_START); + status.set_valign(Gtk::ALIGN_CENTER); + status.set_size_request(1, -1); + status.set_markup(""); + status.set_line_wrap(true); + status.get_style_context()->add_class("inksmall"); + status_box.pack_start(status, TRUE, TRUE, 0); + _getContents()->pack_end(status_box, false, false, 2); + + _message_stack = std::make_shared(); + _message_context = std::unique_ptr(new Inkscape::MessageContext(_message_stack)); + _message_changed_connection = + _message_stack->connectChanged(sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj()))); + + _valueRenderer = Gtk::manage(new Gtk::CellRendererText()); + _valueRenderer->property_editable() = true; + _valueRenderer->property_placeholder_text().set_value(_("Attribute Value")); + _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END); + _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited)); + _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit)); + _treeView.append_column(_("Value"), *_valueRenderer); + _valueCol = _treeView.get_column(2); + if (_valueCol) { + _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender); + } + _popover = Gtk::manage(new Gtk::Popover()); + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + _textview = Gtk::manage(new Gtk::TextView()); + _textview->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR); + _textview->set_editable(true); + _textview->set_monospace(true); + _textview->set_border_width(6); + _textview->signal_map().connect(sigc::mem_fun(*this, &AttrDialog::textViewMap)); + Glib::RefPtr textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.add(*_textview); + _scrolled_text_view.set_max_content_height(450); + _scrolled_text_view.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scrolled_text_view.set_propagate_natural_width(true); + Gtk::Label *helpreturn = Gtk::manage(new Gtk::Label(_("Shift+Return new line"))); + helpreturn->get_style_context()->add_class("inksmall"); + Gtk::Button *apply = Gtk::manage(new Gtk::Button()); + Gtk::Image *icon = Gtk::manage(sp_get_icon_image("on-outline", 26)); + apply->set_relief(Gtk::RELIEF_NONE); + icon->show(); + apply->add(*icon); + apply->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueEditedPop)); + Gtk::Button *cancel = Gtk::manage(new Gtk::Button()); + icon = Gtk::manage(sp_get_icon_image("off-outline", 26)); + cancel->set_relief(Gtk::RELIEF_NONE); + icon->show(); + cancel->add(*icon); + cancel->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueCanceledPop)); + hbox->pack_end(*apply, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*cancel, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*helpreturn, Gtk::PACK_SHRINK, 3); + vbox->pack_start(_scrolled_text_view, Gtk::PACK_EXPAND_WIDGET, 3); + vbox->pack_start(*hbox, Gtk::PACK_EXPAND_WIDGET, 3); + _popover->add(*vbox); + _popover->show(); + _popover->set_relative_to(_treeView); + _popover->set_position(Gtk::PositionType::POS_BOTTOM); + _popover->signal_closed().connect(sigc::mem_fun(*this, &AttrDialog::popClosed)); + _popover->get_style_context()->add_class("attrpop"); + attr_reset_context(0); + _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + setDesktop(getDesktop()); + // I couldent get the signal go well not using C way signals + g_signal_connect(GTK_WIDGET(_popover->gobj()), "key-press-event", G_CALLBACK(key_callback), this); + _popover->hide(); + _updating = false; +} + +void AttrDialog::textViewMap() +{ + auto vscroll = _scrolled_text_view.get_vadjustment(); + int height = vscroll->get_upper() + 12; // padding 6+6 + if (height < 450) { + _scrolled_text_view.set_min_content_height(height); + vscroll->set_value(vscroll->get_lower()); + } else { + _scrolled_text_view.set_min_content_height(450); + } +} + +gboolean sp_show_pop_map(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast(data); + attrdialog->textViewMap(); + return FALSE; +} + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog) +{ + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (attrdialog->_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + attrdialog->valueEditedPop(); + attrdialog->_popover->hide(); + return true; + } else { + g_timeout_add(50, &sp_show_pop_map, attrdialog); + } + } + } break; + } + return false; +} + +/** + * Prepare value string suitable for display in a Gtk::CellRendererText + * + * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added. + * Overall length is limited as well to prevent performance degradation for very long values. + * + * @param value Raw attribute value as UTF-8 encoded string + * @return Single-line string with fixed maximum length + */ +static Glib::ustring prepare_rendervalue(const char *value) +{ + constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons + // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty + + Glib::ustring renderval; + + // truncate to MAX_LENGTH + if (g_utf8_strlen(value, -1) > MAX_LENGTH) { + renderval = Glib::ustring(value, MAX_LENGTH) + "…"; + } else { + renderval = value; + } + + // truncate at first newline (if present) and add a visual indicator + auto ind = renderval.find('\n'); + if (ind != Glib::ustring::npos) { + renderval.replace(ind, Glib::ustring::npos, " ⏎ …"); + } + + return renderval; +} + + +/** + * @brief AttrDialog::~AttrDialog + * Class destructor + */ +AttrDialog::~AttrDialog() +{ + setDesktop(nullptr); + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; + _message_changed_connection.~connection(); +} + +void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast(cell); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry)); +} + + +gboolean sp_show_attr_pop(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast(data); + attrdialog->_popover->show_all(); + + return FALSE; +} + +gboolean sp_close_entry(gpointer data) +{ + Gtk::CellEditable *cell = reinterpret_cast(data); + if (cell) { + cell->property_editing_canceled() = true; + cell->remove_widget(); + } + return FALSE; +} + +void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast(cell); + int width = 0; + int height = 0; + int colwidth = _valueCol->get_width(); + _textview->set_size_request(510, -1); + _popover->set_size_request(520, -1); + valuepath = path; + entry->get_layout()->get_pixel_size(width, height); + Gtk::TreeIter iter = *_store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || colwidth - 10 < width || + name == "content") { + valueediting = entry->get_text(); + Gdk::Rectangle rect; + _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect); + if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) { + rect.set_y(rect.get_y() + 20); + } + _popover->set_pointing_to(rect); + Glib::RefPtr textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(row[_attrColumns._attributeValue]); + _textview->set_buffer(textbuffer); + g_timeout_add(50, &sp_close_entry, cell); + g_timeout_add(50, &sp_show_attr_pop, this); + } else { + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry)); + } + } +} + +void AttrDialog::popClosed() +{ + Glib::RefPtr textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.set_min_content_height(20); +} + +/** + * @brief AttrDialog::setDesktop + * @param desktop + * This function sets the 'desktop' for the CSS pane. + */ +void AttrDialog::setDesktop(SPDesktop* desktop) +{ + _desktop = desktop; +} + +/** + * @brief AttrDialog::setRepr + * Set the internal xml object that I'm working on right now. + */ +void AttrDialog::setRepr(Inkscape::XML::Node * repr) +{ + if ( repr == _repr ) return; + if (_repr) { + _store->clear(); + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + _repr = repr; + if (repr) { + Inkscape::GC::anchor(_repr); + _repr->addListener(&_repr_events, this); + _repr->synthesizeEvents(&_repr_events, this); + } +} + +void AttrDialog::setUndo(Glib::ustring const &event_description) +{ + SPDocument *document = this->_desktop->doc(); + DocumentUndo::done(document, SP_VERB_DIALOG_XML_EDITOR, event_description); +} + +void AttrDialog::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget) +{ + if (widget) { + gtk_label_set_markup(GTK_LABEL(widget), message ? message : ""); + } +} + +/** + * Sets the AttrDialog status bar, depending on which attr is selected. + */ +void AttrDialog::attr_reset_context(gint attr) +{ + if (attr == 0) { + _message_context->set(Inkscape::NORMAL_MESSAGE, _("Click attribute to edit.")); + } else { + const gchar *name = g_quark_to_string(attr); + _message_context->setF( + Inkscape::NORMAL_MESSAGE, + _("Attribute %s selected. Press Ctrl+Enter when done editing to commit changes."), name); + } +} + +/** + * @brief AttrDialog::onAttrChanged + * This is called when the XML has an updated attribute + */ +void AttrDialog::onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value) +{ + if (_updating) { + return; + } + Glib::ustring renderval; + if (new_value) { + renderval = prepare_rendervalue(new_value); + } + for(auto iter: this->_store->children()) + { + Gtk::TreeModel::Row row = *iter; + Glib::ustring col_name = row[_attrColumns._attributeName]; + if(name == col_name) { + if(new_value) { + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + new_value = nullptr; // Don't make a new one + } else { + _store->erase(iter); + } + break; + } + } + if (new_value) { + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_attrColumns._attributeName] = name; + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + } +} + +/** + * @brief AttrDialog::onAttrCreate + * This function is a slot to signal_clicked for '+' button panel. + */ +bool AttrDialog::onAttrCreate(GdkEventButton *event) +{ + if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) { + Gtk::TreeIter iter = _store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + _treeView.set_cursor(path, *_nameCol, true); + grab_focus(); + return true; + } + return false; +} + +/** + * @brief AttrDialog::onAttrDelete + * @param event + * @return true + * Delete the attribute from the xml + */ +void AttrDialog::onAttrDelete(Glib::ustring path) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if (row) { + Glib::ustring name = row[_attrColumns._attributeName]; + if (name == "content") { + return; + } else { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + } +} + +/** + * @brief AttrDialog::onKeyPressed + * @param event + * @return true + * Delete or create elements based on key presses + */ +bool AttrDialog::onKeyPressed(GdkEventKey *event) +{ + bool ret = false; + if(this->_repr) { + auto selection = this->_treeView.get_selection(); + Gtk::TreeModel::Row row = *(selection->get_selected()); + Gtk::TreeIter iter = *(selection->get_selected()); + switch (event->keyval) + { + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: { + // Create new attribute (repeat code, fold into above event!) + Glib::ustring name = row[_attrColumns._attributeName]; + if (name != "content") { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + ret = true; + } break; + case GDK_KEY_plus: + case GDK_KEY_Insert: + { + // Create new attribute (repeat code, fold into above event!) + Gtk::TreeIter iter = this->_store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + this->_treeView.set_cursor(path, *this->_nameCol, true); + grab_focus(); + ret = true; + } break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + valueEditedPop(); + _popover->hide(); + ret = true; + } + } + } break; + } + } + return ret; +} + +bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + + +bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +gboolean sp_attrdialog_store_move_to_next(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast(data); + auto selection = attrdialog->_treeView.get_selection(); + Gtk::TreeIter iter = *(selection->get_selected()); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + Gtk::TreeViewColumn *focus_column; + attrdialog->_treeView.get_cursor(path, focus_column); + if (path == attrdialog->_modelpath && focus_column == attrdialog->_treeView.get_column(1)) { + attrdialog->_treeView.set_cursor(attrdialog->_modelpath, *attrdialog->_valueCol, true); + } + return FALSE; +} + +/** + * + * + * @brief AttrDialog::nameEdited + * @param event + * @return + * Called when the name is edited in the TreeView editable column + */ +void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name) +{ + Gtk::TreeIter iter = *_store->get_iter(path); + _modelpath = (Gtk::TreeModel::Path)iter; + Gtk::TreeModel::Row row = *iter; + if(row && this->_repr) { + Glib::ustring old_name = row[_attrColumns._attributeName]; + if (old_name == name) { + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + grab_focus(); + return; + } + if (old_name == "content") { + return; + } + // Do not allow empty name (this would delete the attribute) + if (name.empty()) { + return; + } + // Do not allow duplicate names + const auto children = _store->children(); + for (const auto &child : children) { + if (name == child[_attrColumns._attributeName]) { + return; + } + } + if(std::any_of(name.begin(), name.end(), isspace)) { + return; + } + // Copy old value and remove old name + Glib::ustring value; + if (!old_name.empty()) { + value = row[_attrColumns._attributeValue]; + _updating = true; + _repr->removeAttribute(old_name); + _updating = false; + } + + // Do the actual renaming and set new value + row[_attrColumns._attributeName] = name; + grab_focus(); + _updating = true; + _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values) + _updating = false; + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + this->setUndo(_("Rename attribute")); + } +} + +void AttrDialog::valueEditedPop() +{ + Glib::ustring value = _textview->get_buffer()->get_text(); + valueEdited(valuepath, value); + valueediting = ""; + _popover->hide(); +} + +void AttrDialog::valueCanceledPop() +{ + if (!valueediting.empty()) { + Glib::RefPtr textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(valueediting); + _textview->set_buffer(textbuffer); + } + _popover->hide(); +} + +/** + * @brief AttrDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if(row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + Glib::ustring old_value = row[_attrColumns._attributeValue]; + if (old_value == value) { + return; + } + if(name.empty()) return; + if (name == "content") { + _repr->setContent(value.c_str()); + } else { + _repr->setAttributeOrRemoveIfEmpty(name, value); + } + if(!value.empty()) { + row[_attrColumns._attributeValue] = value; + Glib::ustring renderval = prepare_rendervalue(value.c_str()); + row[_attrColumns._attributeValueRender] = renderval; + } + Inkscape::Selection *selection = _desktop->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + this->setUndo(_("Change attribute value")); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h new file mode 100644 index 0000000..23437f0 --- /dev/null +++ b/src/ui/dialog/attrdialog.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes based on Gtk TreeView + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_DIALOGS_ATTRDIALOG_H +#define SEEN_UI_DIALOGS_ATTRDIALOG_H + +#include "desktop.h" +#include "message.h" +#include +#include +#include +#include +#include +#include +#include + +#define ATTR_DIALOG(obj) (dynamic_cast((Inkscape::UI::Dialog::AttrDialog*)obj)) + +namespace Inkscape { +class MessageStack; +class MessageContext; +namespace UI { +namespace Dialog { + +/** + * @brief The AttrDialog class + * This dialog allows to add, delete and modify XML attributes created in the + * xml editor. + */ +class AttrDialog : public UI::Widget::Panel +{ +public: + AttrDialog(); + ~AttrDialog() override; + + static AttrDialog &getInstance() { return *new AttrDialog(); } + + // Data structure + class AttrColumns : public Gtk::TreeModel::ColumnRecord { + public: + AttrColumns() { + add(_attributeName); + add(_attributeValue); + add(_attributeValueRender); + } + Gtk::TreeModelColumn _attributeName; + Gtk::TreeModelColumn _attributeValue; + Gtk::TreeModelColumn _attributeValueRender; + }; + AttrColumns _attrColumns; + + // TreeView + Gtk::TreeView _treeView; + Glib::RefPtr _store; + Gtk::CellRendererText *_nameRenderer; + Gtk::CellRendererText *_valueRenderer; + Gtk::TreeViewColumn *_nameCol; + Gtk::TreeViewColumn *_valueCol; + Gtk::TreeModel::Path _modelpath; + Gtk::Popover *_popover; + Gtk::TextView *_textview; + Glib::ustring valuepath; + Glib::ustring valueediting; + + /** + * Status bar + */ + std::shared_ptr _message_stack; + std::unique_ptr _message_context; + + // Widgets + Gtk::VBox _mainBox; + Gtk::ScrolledWindow _scrolledWindow; + Gtk::ScrolledWindow _scrolled_text_view; + Gtk::HBox _buttonBox; + Gtk::Button _buttonAddAttribute; + // Variables - Inkscape + SPDesktop* _desktop; + Inkscape::XML::Node* _repr; + Gtk::HBox status_box; + Gtk::Label status; + bool _updating; + + // Helper functions + void setDesktop(SPDesktop* desktop) override; + void setRepr(Inkscape::XML::Node * repr); + void setUndo(Glib::ustring const &event_description); + /** + * Sets the XML status bar, depending on which attr is selected. + */ + void attr_reset_context(gint attr); + static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog); + + /** + * Signal handlers + */ + sigc::connection _message_changed_connection; + void onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value); + bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + void onAttrDelete(Glib::ustring path); + bool onAttrCreate(GdkEventButton *event); + bool onKeyPressed(GdkEventKey *event); + void popClosed(); + void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void nameEdited(const Glib::ustring &path, const Glib::ustring &name); + void valueEdited(const Glib::ustring &path, const Glib::ustring &value); + void textViewMap(); + void valueCanceledPop(); + void valueEditedPop(); +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // ATTRDIALOG_H diff --git a/src/ui/dialog/behavior.h b/src/ui/dialog/behavior.h new file mode 100644 index 0000000..1b6c7c7 --- /dev/null +++ b/src/ui/dialog/behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog behavior interface + */ +/* Author: + * Gustav Broberg + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_BEHAVIOR_H + +#include +#include + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Dialog; + +namespace Behavior { + +class Behavior; + +typedef Behavior *(*BehaviorFactory)(Dialog &dialog); + +template +Behavior *create(Dialog &dialog) +{ + return T::create(dialog); +} + + +class Behavior { + +public: + virtual ~Behavior() = default; + + /** Gtk::Dialog methods */ + virtual operator Gtk::Widget&() =0; + virtual GtkWidget *gobj() =0; + virtual void present() =0; + virtual Gtk::Box *get_vbox() =0; + virtual void show() =0; + virtual void hide() =0; + virtual void show_all_children() =0; + virtual void resize(int width, int height) =0; + virtual void move(int x, int y) =0; + virtual void set_position(Gtk::WindowPosition) =0; + virtual void set_size_request(int width, int height) =0; + virtual void size_request(Gtk::Requisition &requisition) =0; + virtual void get_position(int &x, int &y) =0; + virtual void get_size(int &width, int &height) =0; + virtual void set_title(Glib::ustring title) =0; + virtual void set_sensitive(bool sensitive) =0; + + /** Gtk::Dialog signal proxies */ + virtual Glib::SignalProxy0 signal_show() =0; + virtual Glib::SignalProxy0 signal_hide() =0; + virtual Glib::SignalProxy1 signal_delete_event() =0; + + /** Custom signal handlers */ + virtual void onHideF12() =0; + virtual void onShowF12() =0; + virtual void onShutdown() =0; + virtual void onDesktopActivated(SPDesktop *desktop) =0; + +protected: + Behavior(Dialog &dialog) + : _dialog (dialog) + { } + + Dialog& _dialog; //< reference to the owner + +private: + Behavior() = delete; // no constructor without params + Behavior(const Behavior &) = delete; // no copy + Behavior &operator=(const Behavior &) = delete; // no assign +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif //INKSCAPE_UI_DIALOG_BEHAVIOR_H + +/* + 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 : diff --git a/src/ui/dialog/calligraphic-profile-rename.cpp b/src/ui/dialog/calligraphic-profile-rename.cpp new file mode 100644 index 0000000..604ac7f --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for naming calligraphic profiles. + * + * @note This file is in the wrong directory because of link order issues - + * it is required by widgets/toolbox.cpp, and libspwidgets.a comes after + * libinkdialogs.a in the current link order. + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "calligraphic-profile-rename.h" +#include +#include + +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +CalligraphicProfileRename::CalligraphicProfileRename() : + _layout_table(Gtk::manage(new Gtk::Grid())), + _applied(false) +{ + set_title(_("Edit profile")); + + auto mainVBox = get_content_area(); + _layout_table->set_column_spacing(4); + _layout_table->set_row_spacing(4); + + _profile_name_entry.set_activates_default(true); + + _profile_name_label.set_label(_("Profile name:")); + _profile_name_label.set_halign(Gtk::ALIGN_END); + _profile_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table->attach(_profile_name_label, 0, 0, 1, 1); + + _profile_name_entry.set_hexpand(); + _layout_table->attach(_profile_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(*_layout_table, false, false, 4); + // Buttons + _close_button.set_use_underline(); + _close_button.set_label(_("_Cancel")); + _close_button.set_can_default(); + + _delete_button.set_use_underline(true); + _delete_button.set_label(_("_Delete")); + _delete_button.set_can_default(); + _delete_button.set_visible(false); + + _apply_button.set_use_underline(true); + _apply_button.set_label(_("_Save")); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)); + _delete_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_delete)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_apply)); + + signal_delete_event().connect( sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)), true ) ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_delete_button, Gtk::RESPONSE_DELETE_EVENT); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +void CalligraphicProfileRename::_apply() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = false; + _close(); +} + +void CalligraphicProfileRename::_delete() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = true; + _close(); +} + +void CalligraphicProfileRename::_close() +{ + this->Gtk::Dialog::hide(); +} + +void CalligraphicProfileRename::show(SPDesktop *desktop, const Glib::ustring profile_name) +{ + CalligraphicProfileRename &dial = instance(); + dial._applied=false; + dial._deleted=false; + dial.set_modal(true); + + dial._profile_name = profile_name; + dial._profile_name_entry.set_text(profile_name); + + if (profile_name.empty()) { + dial.set_title(_("Add profile")); + dial._delete_button.set_visible(false); + + } else { + dial.set_title(_("Edit profile")); + dial._delete_button.set_visible(true); + } + + desktop->setWindowTransient (dial.gobj()); + dial.property_destroy_with_parent() = true; + // dial.Gtk::Dialog::show(); + //dial.present(); + dial.run(); +} + +} // 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 : diff --git a/src/ui/dialog/calligraphic-profile-rename.h b/src/ui/dialog/calligraphic-profile-rename.h new file mode 100644 index 0000000..195275b --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for naming calligraphic profiles + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H +#define INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H + +#include +#include +#include + +namespace Gtk { +class Grid; +} + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class CalligraphicProfileRename : public Gtk::Dialog { +public: + CalligraphicProfileRename(); + ~CalligraphicProfileRename() override = default; + Glib::ustring getName() const { + return "CalligraphicProfileRename"; + } + + static void show(SPDesktop *desktop, const Glib::ustring profile_name); + static bool applied() { + return instance()._applied; + } + static bool deleted() { + return instance()._deleted; + } + static Glib::ustring getProfileName() { + return instance()._profile_name; + } + +protected: + void _close(); + void _apply(); + void _delete(); + + Gtk::Label _profile_name_label; + Gtk::Entry _profile_name_entry; + Gtk::Grid* _layout_table; + + Gtk::Button _close_button; + Gtk::Button _delete_button; + Gtk::Button _apply_button; + Glib::ustring _profile_name; + bool _applied; + bool _deleted; +private: + static CalligraphicProfileRename &instance() { + static CalligraphicProfileRename instance_; + return instance_; + } + CalligraphicProfileRename(CalligraphicProfileRename const &) = delete; // no copy + CalligraphicProfileRename &operator=(CalligraphicProfileRename const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H + +/* + 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 : diff --git a/src/ui/dialog/clonetiler.cpp b/src/ui/dialog/clonetiler.cpp new file mode 100644 index 0000000..edc88ba --- /dev/null +++ b/src/ui/dialog/clonetiler.cpp @@ -0,0 +1,2834 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Clone tiling dialog + */ +/* Authors: + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * Romain de Bossoreille + * + * Copyright (C) 2004-2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "clonetiler.h" + +#include + +#include +#include +#include +#include +#include + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "message-stack.h" +#include "unclump.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "ui/icon-loader.h" + +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-use.h" + +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/unit-menu.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SB_MARGIN 1 +#define VB_MARGIN 4 + +static Glib::ustring const prefs_path = "/dialogs/clonetiler/"; + +static Inkscape::Drawing *trace_drawing = nullptr; +static unsigned trace_visionkey; +static gdouble trace_zoom; +static SPDocument *trace_doc = nullptr; + +CloneTiler::CloneTiler () : + UI::Widget::Panel("/dialogs/clonetiler/", SP_VERB_DIALOG_CLONETILER), + desktop(nullptr), + deskTrack(), + table_row_labels(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + + { + auto prefs = Inkscape::Preferences::get(); + + auto mainbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(mainbox), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (mainbox), 6); + + contents->pack_start (*Gtk::manage(Glib::wrap(mainbox)), true, true, 0); + + nb = gtk_notebook_new (); + gtk_box_pack_start (GTK_BOX (mainbox), nb, FALSE, FALSE, 0); + + + // Symmetry + { + GtkWidget *vb = new_tab (nb, _("_Symmetry")); + + /* TRANSLATORS: For the following 17 symmetry groups, see + * http://www.bib.ulb.ac.be/coursmath/doc/17.htm (visual examples); + * http://www.clarku.edu/~djoyce/wallpaper/seventeen.html (English vocabulary); or + * http://membres.lycos.fr/villemingerard/Geometri/Sym1D.htm (French vocabulary). + */ + struct SymGroups { + gint group; + Glib::ustring label; + } const sym_groups[] = { + // TRANSLATORS: "translation" means "shift" / "displacement" here. + {TILE_P1, _("P1: simple translation")}, + {TILE_P2, _("P2: 180° rotation")}, + {TILE_PM, _("PM: reflection")}, + // TRANSLATORS: "glide reflection" is a reflection and a translation combined. + // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html + {TILE_PG, _("PG: glide reflection")}, + {TILE_CM, _("CM: reflection + glide reflection")}, + {TILE_PMM, _("PMM: reflection + reflection")}, + {TILE_PMG, _("PMG: reflection + 180° rotation")}, + {TILE_PGG, _("PGG: glide reflection + 180° rotation")}, + {TILE_CMM, _("CMM: reflection + reflection + 180° rotation")}, + {TILE_P4, _("P4: 90° rotation")}, + {TILE_P4M, _("P4M: 90° rotation + 45° reflection")}, + {TILE_P4G, _("P4G: 90° rotation + 90° reflection")}, + {TILE_P3, _("P3: 120° rotation")}, + {TILE_P31M, _("P31M: reflection + 120° rotation, dense")}, + {TILE_P3M1, _("P3M1: reflection + 120° rotation, sparse")}, + {TILE_P6, _("P6: 60° rotation")}, + {TILE_P6M, _("P6M: reflection + 60° rotation")}, + }; + + gint current = prefs->getInt(prefs_path + "symmetrygroup", 0); + + // Add a new combo box widget with the list of symmetry groups to the vbox + auto combo = Gtk::manage(new Gtk::ComboBoxText()); + combo->set_tooltip_text(_("Select one of the 17 symmetry groups for the tiling")); + + // Hack to add markup support + auto cell_list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo->gobj())); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo->gobj()), + GTK_CELL_RENDERER(cell_list->data), + "markup", 0, NULL); + + for (const auto & sg : sym_groups) { + // Add the description of the symgroup to a new row + combo->append(sg.label); + } + + gtk_box_pack_start (GTK_BOX (vb), GTK_WIDGET(combo->gobj()), FALSE, FALSE, SB_MARGIN); + + combo->set_active(current); + combo->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::symgroup_changed), combo)); + } + + table_row_labels = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + // Shift + { + GtkWidget *vb = new_tab (nb, _("S_hift")); + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("Shift X:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per row (in % of tile width)"), "shiftx_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per column (in % of tile width)"), "shiftx_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal shift by this percentage"), "shiftx_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("Shift Y:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per row (in % of tile height)"), "shifty_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per column (in % of tile height)"), "shifty_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox ( + _("Randomize the vertical shift by this percentage"), "shifty_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Exponent:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox ( + _("Whether rows are spaced evenly (1), converge (<1) or diverge (>1)"), "shifty_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox ( + _("Whether columns are spaced evenly (1), converge (<1) or diverge (>1)"), "shiftx_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each row"), "shifty_alternate"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each column"), "shiftx_alternate"); + table_attach (table, l, 0, 5, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Cumulate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Cumulate the shifts for each row"), "shifty_cumulate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Cumulate the shifts for each column"), "shiftx_cumulate"); + table_attach (table, l, 0, 6, 3); + } + + { // Exclude tile width and height in shift + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Exclude tile:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Exclude tile height in shift"), "shifty_excludeh"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Exclude tile width in shift"), "shiftx_excludew"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Scale + { + GtkWidget *vb = new_tab (nb, _("Sc_ale")); + + GtkWidget *table = table_x_y_rand (2); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Scale X:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per row (in % of tile width)"), "scalex_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per column (in % of tile width)"), "scalex_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal scale by this percentage"), "scalex_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Scale Y:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per row (in % of tile height)"), "scaley_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per column (in % of tile height)"), "scaley_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the vertical scale by this percentage"), "scaley_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Exponent:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Whether row scaling is uniform (1), converge (<1) or diverge (>1)"), "scaley_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Whether column scaling is uniform (1), converge (<1) or diverge (>1)"), "scalex_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + // Logarithmic (as in logarithmic spiral) + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Base:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scaley_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scalex_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 3); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each row"), "scaley_alternate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each column"), "scalex_alternate"); + table_attach (table, l, 0, 6, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Cumulate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Cumulate the scales for each row"), "scaley_cumulate"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Cumulate the scales for each column"), "scalex_cumulate"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Rotation + { + GtkWidget *vb = new_tab (nb, _("_Rotation")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Angle + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Angle:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each row"), "rotate_per_j", + -180, 180, "°"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each column"), "rotate_per_i", + -180, 180, "°"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the rotation angle by this percentage"), "rotate_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each row"), "rotate_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each column"), "rotate_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Cumulate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = checkbox (_("Cumulate the rotation for each row"), "rotate_cumulatej"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = checkbox (_("Cumulate the rotation for each column"), "rotate_cumulatei"); + table_attach (table, l, 0, 4, 3); + } + + } + + + // Blur and opacity + { + GtkWidget *vb = new_tab (nb, _("_Blur & opacity")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + + // Blur + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Blur:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each row"), "blur_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each column"), "blur_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile blur by this percentage"), "blur_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each row"), "blur_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each column"), "blur_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + + + // Dissolve + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Opacity:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each row"), "opacity_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each column"), "opacity_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the tile opacity by this percentage"), "opacity_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each row"), "opacity_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each column"), "opacity_alternatei"); + table_attach (table, l, 0, 5, 3); + } + } + + + // Color + { + GtkWidget *vb = new_tab (nb, _("Co_lor")); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *l = gtk_label_new (_("Initial color: ")); + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + + guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff); + color_picker = new Inkscape::UI::Widget::ColorPicker (*new Glib::ustring(_("Initial color of tiled clones")), *new Glib::ustring(_("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)")), rgba, false); + color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed)); + + gtk_box_pack_start (GTK_BOX (hb), reinterpret_cast(color_picker->gobj()), FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + } + + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Hue + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("H:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each row"), "hue_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each column"), "hue_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile hue by this percentage"), "hue_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + + // Saturation + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("S:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each row"), "saturation_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each column"), "saturation_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the color saturation by this percentage"), "saturation_rand", + 0, 100, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Lightness + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("L:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each row"), "lightness_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each column"), "lightness_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the color lightness by this percentage"), "lightness_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + + { // alternates + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Alternate:")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each row"), "color_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each column"), "color_alternatei"); + table_attach (table, l, 0, 5, 3); + } + + } + + // Trace + { + GtkWidget *vb = new_tab (nb, _("_Trace")); + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_container_set_border_width(GTK_CONTAINER(hb), 4); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + + _b = Gtk::manage(new Gtk::CheckButton(_("Trace the drawing under the clones/sprayed items"))); + _b->set_data("uncheckable", GINT_TO_POINTER(TRUE)); + bool old = prefs->getBool(prefs_path + "dotrace"); + _b->set_active(old); + _b->set_tooltip_text(_("For each clone/sprayed item, pick a value from the drawing in its location and apply it")); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(_b->gobj()), FALSE, FALSE, 0); + _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled)); + } + + { + auto vvb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_set_homogeneous(GTK_BOX(vvb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), vvb, FALSE, FALSE, 0); + _dotrace = vvb; + + { + GtkWidget *frame = gtk_frame_new (_("1. Pick from the drawing:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Color"))); + radio->set_tooltip_text(_("Pick the visible color and opacity")); + table_attach(table, radio, 0.0, 1, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_COLOR)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_COLOR); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Opacity"))); + radio->set_tooltip_text(_("Pick the total accumulated opacity")); + table_attach (table, radio, 0.0, 2, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_OPACITY)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_OPACITY); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("R"))); + radio->set_tooltip_text(_("Pick the Red component of the color")); + table_attach (table, radio, 0.0, 1, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_R)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_R); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("G"))); + radio->set_tooltip_text(_("Pick the Green component of the color")); + table_attach (table, radio, 0.0, 2, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_G)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_G); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("B"))); + radio->set_tooltip_text(_("Pick the Blue component of the color")); + table_attach (table, radio, 0.0, 3, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_B)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_B); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color hue", "H"))); + radio->set_tooltip_text(_("Pick the hue of the color")); + table_attach (table, radio, 0.0, 1, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_H)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_H); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color saturation", "S"))); + radio->set_tooltip_text(_("Pick the saturation of the color")); + table_attach (table, radio, 0.0, 2, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_S)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_S); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color lightness", "L"))); + radio->set_tooltip_text(_("Pick the lightness of the color")); + table_attach (table, radio, 0.0, 3, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_L)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_L); + } + + } + + { + GtkWidget *frame = gtk_frame_new (_("2. Tweak the picked value:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, VB_MARGIN); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Gamma-correct:")); + table_attach (table, l, 1.0, 1, 1); + } + { + auto l = spinbox (_("Shift the mid-range of the picked value upwards (>0) or downwards (<0)"), "gamma_picked", + -10, 10, ""); + table_attach (table, l, 0.0, 1, 2); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Randomize:")); + table_attach (table, l, 1.0, 1, 3); + } + { + auto l = spinbox (_("Randomize the picked value by this percentage"), "rand_picked", + 0, 100, "%"); + table_attach (table, l, 0.0, 1, 4); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Invert:")); + table_attach (table, l, 1.0, 2, 1); + } + { + auto l = checkbox (_("Invert the picked value"), "invert_picked"); + table_attach (table, l, 0.0, 2, 2); + } + } + + { + GtkWidget *frame = gtk_frame_new (_("3. Apply the value to the clones':")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Presence"))); + bool old = prefs->getBool(prefs_path + "pick_to_presence", true); + b->set_active(old); + b->set_tooltip_text(_("Each clone is created with the probability determined by the picked value in that point")); + table_attach (table, b, 0.0, 1, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_presence")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Size"))); + bool old = prefs->getBool(prefs_path + "pick_to_size"); + b->set_active(old); + b->set_tooltip_text(_("Each clone's size is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_size")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Color"))); + bool old = prefs->getBool(prefs_path + "pick_to_color", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone is painted by the picked color (the original must have unset fill or stroke)")); + table_attach (table, b, 0.0, 1, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_color")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Opacity"))); + bool old = prefs->getBool(prefs_path + "pick_to_opacity", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone's opacity is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_opacity")); + } + } + gtk_widget_set_sensitive (vvb, prefs->getBool(prefs_path + "dotrace")); + } + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup (GTK_LABEL(l), _("Apply to tiled clones:")); + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + // Rows/columns, width/height + { + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + gtk_box_pack_start (GTK_BOX (mainbox), table, FALSE, FALSE, 0); + + { + _rowscols = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "jmax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many rows in the tiling")); + sb->set_width_chars (7); + _rowscols->pack_start(*sb, true, true, 0); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "jmax")); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + _rowscols->pack_start(*l, true, true, 0); + } + + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "imax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many columns in the tiling")); + sb->set_width_chars (7); + _rowscols->pack_start(*sb, true, true, 0); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax")); + } + + table_attach (table, GTK_WIDGET(_rowscols->gobj()), 0.0, 1, 2); + } + + { + _widthheight = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + // unitmenu + unit_menu = new Inkscape::UI::Widget::UnitMenu(); + unit_menu->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + unit_menu->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); + unitChangedConn = unit_menu->signal_changed().connect(sigc::mem_fun(*this, &CloneTiler::unit_changed)); + + { + // Width spinbutton + fill_width = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillwidth", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_width->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_width, 1.0, 2); + e->set_tooltip_text (_("Width of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + _widthheight->pack_start(*e, true, true, 0); + fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed)); + } + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + _widthheight->pack_start(*l, true, true, 0); + } + + { + // Height spinbutton + fill_height = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillheight", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_height->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_height, 1.0, 2); + e->set_tooltip_text (_("Height of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + _widthheight->pack_start(*e, true, true, 0); + fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed)); + } + + _widthheight->pack_start(*unit_menu, true, true, 0); + table_attach (table, GTK_WIDGET(_widthheight->gobj()), 0.0, 2, 2); + + } + + // Switch + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Rows, columns: "))); + radio->set_tooltip_text(_("Create the specified number of rows and columns")); + table_attach (table, GTK_WIDGET(radio->gobj()), 0.0, 1, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create)); + + if (!prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Width, height: "))); + radio->set_tooltip_text(_("Fill the specified width and height with the tiling")); + table_attach (table, GTK_WIDGET(radio->gobj()), 0.0, 2, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill)); + + if (prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + } + + + // Use saved pos + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + gtk_box_pack_start (GTK_BOX (mainbox), GTK_WIDGET(hb->gobj()), FALSE, FALSE, 0); + + _cb_keep_bbox = Gtk::manage(new Gtk::CheckButton(_("Use saved size and position of the tile"))); + auto keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + _cb_keep_bbox->set_active(keepbbox); + _cb_keep_bbox->set_tooltip_text(_("Pretend that the size and position of the tile are the same " + "as the last time you tiled it (if any), instead of using the " + "current size")); + hb->pack_start(*_cb_keep_bbox, false, false, 0); + _cb_keep_bbox->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::keep_bbox_toggled)); + } + + // Statusbar + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_end (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + _status = l; + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + + // Buttons + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + + { + auto b = Gtk::manage(new Gtk::Button()); + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup_with_mnemonic(_(" _Create ")); + b->add(*l); + b->set_tooltip_text(_("Create and tile the clones of the selection")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::apply)); + gtk_box_pack_end (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + { // buttons which are enabled only when there are tiled clones + auto sb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_box_set_homogeneous(GTK_BOX(sb), FALSE); + gtk_box_pack_end (GTK_BOX (hb), sb, FALSE, FALSE, 0); + _buttons_on_tiles = sb; + { + // TRANSLATORS: if a group of objects are "clumped" together, then they + // are unevenly spread in the given amount of space - as shown in the + // diagrams on the left in the following screenshot: + // http://www.inkscape.org/screenshots/gallery/inkscape-0.42-CVS-tiles-unclump.png + // So unclumping is the process of spreading a number of objects out more evenly. + auto b = Gtk::manage(new Gtk::Button(_(" _Unclump "), true)); + b->set_tooltip_text(_("Spread out clones to reduce clumping; can be applied repeatedly")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::unclump)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" Re_move "), true)); + b->set_tooltip_text(_("Remove existing tiled clones of the selected object (siblings only)")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::on_remove_button_clicked)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + // connect to global selection changed signal (so we can change desktops) and + // external_change (so we're not fooled by undo) + selectChangedConn = INKSCAPE.signal_selection_changed.connect(sigc::mem_fun(*this, &CloneTiler::change_selection)); + externChangedConn = INKSCAPE.signal_external_change.connect(sigc::mem_fun(*this, &CloneTiler::external_change)); + + // update now + change_selection(SP_ACTIVE_DESKTOP->getSelection()); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" R_eset "), true)); + // TRANSLATORS: "change" is a noun here + b->set_tooltip_text(_("Reset all shifts, scales, rotates, opacity and color changes in the dialog to zero")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::reset)); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + } + + gtk_widget_show_all (mainbox); + } + + show_all(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &CloneTiler::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + +} + +CloneTiler::~CloneTiler () +{ + //subselChangedConn.disconnect(); + //selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); + externChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); + color_changed_connection.disconnect(); +} + +void CloneTiler::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void CloneTiler::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + this->desktop = desktop; + } +} + +void CloneTiler::on_picker_color_changed(guint rgba) +{ + static bool is_updating = false; + if (is_updating || !SP_ACTIVE_DESKTOP) + return; + + is_updating = true; + + gchar c[32]; + sp_svg_write_color(c, sizeof(c), rgba); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(prefs_path + "initial_color", c); + + is_updating = false; +} + +void CloneTiler::change_selection(Inkscape::Selection *selection) +{ + if (selection->isEmpty()) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("Nothing selected.")); + return; + } + + if (boost::distance(selection->items()) > 1) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("More than one object selected.")); + return; + } + + guint n = number_of_clones(selection->singleItem()); + if (n > 0) { + gtk_widget_set_sensitive (_buttons_on_tiles, TRUE); + gchar *sta = g_strdup_printf (_("Object has %d tiled clones."), n); + gtk_label_set_markup (GTK_LABEL(_status), sta); + g_free (sta); + } else { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("Object has no tiled clones.")); + } +} + +void CloneTiler::external_change() +{ + change_selection(SP_ACTIVE_DESKTOP->getSelection()); +} + +Geom::Affine CloneTiler::get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ) +{ + + // Shift (in units of tile width or height) ------------- + double delta_shifti = 0.0; + double delta_shiftj = 0.0; + + if( shiftx_alternate ) { + delta_shifti = (double)(i%2); + } else { + if( shiftx_cumulate ) { // Should the delta shifts be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_shifti = (double)(i*i); + } else { + delta_shifti = (double)i; + } + } + + if( shifty_alternate ) { + delta_shiftj = (double)(j%2); + } else { + if( shifty_cumulate ) { + delta_shiftj = (double)(j*j); + } else { + delta_shiftj = (double)j; + } + } + + // Random shift, only calculate if non-zero. + double delta_shiftx_rand = 0.0; + double delta_shifty_rand = 0.0; + if( shiftx_rand != 0.0 ) delta_shiftx_rand = shiftx_rand * g_random_double_range (-1, 1); + if( shifty_rand != 0.0 ) delta_shifty_rand = shifty_rand * g_random_double_range (-1, 1); + + + // Delta shift (units of tile width/height) + double di = shiftx_per_i * delta_shifti + shiftx_per_j * delta_shiftj + delta_shiftx_rand; + double dj = shifty_per_i * delta_shifti + shifty_per_j * delta_shiftj + delta_shifty_rand; + + // Shift in actual x and y, used below + double dx = w * di; + double dy = h * dj; + + double shifti = di; + double shiftj = dj; + + // Include tile width and height in shift if required + if( !shiftx_excludew ) shifti += i; + if( !shifty_excludeh ) shiftj += j; + + // Add exponential shift if necessary + double shifti_sign = (shifti > 0.0) ? 1.0 : -1.0; + shifti = shifti_sign * pow(fabs(shifti), shiftx_exp); + double shiftj_sign = (shiftj > 0.0) ? 1.0 : -1.0; + shiftj = shiftj_sign * pow(fabs(shiftj), shifty_exp); + + // Final shift + Geom::Affine rect_translate (Geom::Translate (w * shifti, h * shiftj)); + + // Rotation (in degrees) ------------ + double delta_rotationi = 0.0; + double delta_rotationj = 0.0; + + if( rotate_alternatei ) { + delta_rotationi = (double)(i%2); + } else { + if( rotate_cumulatei ) { + delta_rotationi = (double)(i*i + i)/2.0; + } else { + delta_rotationi = (double)i; + } + } + + if( rotate_alternatej ) { + delta_rotationj = (double)(j%2); + } else { + if( rotate_cumulatej ) { + delta_rotationj = (double)(j*j + j)/2.0; + } else { + delta_rotationj = (double)j; + } + } + + double delta_rotate_rand = 0.0; + if( rotate_rand != 0.0 ) delta_rotate_rand = rotate_rand * 180.0 * g_random_double_range (-1, 1); + + double dr = rotate_per_i * delta_rotationi + rotate_per_j * delta_rotationj + delta_rotate_rand; + + // Scale (times the original) ----------- + double delta_scalei = 0.0; + double delta_scalej = 0.0; + + if( scalex_alternate ) { + delta_scalei = (double)(i%2); + } else { + if( scalex_cumulate ) { // Should the delta scales be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_scalei = (double)(i*i + i)/2.0; + } else { + delta_scalei = (double)i; + } + } + + if( scaley_alternate ) { + delta_scalej = (double)(j%2); + } else { + if( scaley_cumulate ) { + delta_scalej = (double)(j*j + j)/2.0; + } else { + delta_scalej = (double)j; + } + } + + // Random scale, only calculate if non-zero. + double delta_scalex_rand = 0.0; + double delta_scaley_rand = 0.0; + if( scalex_rand != 0.0 ) delta_scalex_rand = scalex_rand * g_random_double_range (-1, 1); + if( scaley_rand != 0.0 ) delta_scaley_rand = scaley_rand * g_random_double_range (-1, 1); + // But if random factors are same, scale x and y proportionally + if( scalex_rand == scaley_rand ) delta_scalex_rand = delta_scaley_rand; + + // Total delta scale + double scalex = 1.0 + scalex_per_i * delta_scalei + scalex_per_j * delta_scalej + delta_scalex_rand; + double scaley = 1.0 + scaley_per_i * delta_scalei + scaley_per_j * delta_scalej + delta_scaley_rand; + + if( scalex < 0.0 ) scalex = 0.0; + if( scaley < 0.0 ) scaley = 0.0; + + // Add exponential scale if necessary + if ( scalex_exp != 1.0 ) scalex = pow( scalex, scalex_exp ); + if ( scaley_exp != 1.0 ) scaley = pow( scaley, scaley_exp ); + + // Add logarithmic factor if necessary + if ( scalex_log > 0.0 ) scalex = pow( scalex_log, scalex - 1.0 ); + if ( scaley_log > 0.0 ) scaley = pow( scaley_log, scaley - 1.0 ); + // Alternative using rotation angle + //if ( scalex_log != 1.0 ) scalex *= pow( scalex_log, M_PI*dr/180 ); + //if ( scaley_log != 1.0 ) scaley *= pow( scaley_log, M_PI*dr/180 ); + + + // Calculate transformation matrices, translating back to "center of tile" (rotation center) before transforming + Geom::Affine drot_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI*dr/180) * Geom::Translate(cx, cy); + + Geom::Affine dscale_c = Geom::Translate(-cx, -cy) * Geom::Scale (scalex, scaley) * Geom::Translate(cx, cy); + + Geom::Affine d_s_r = dscale_c * drot_c; + + Geom::Affine rotate_180_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI) * Geom::Translate(cx, cy); + + Geom::Affine rotate_90_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/2) * Geom::Translate(cx, cy); + Geom::Affine rotate_m90_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/2) * Geom::Translate(cx, cy); + + Geom::Affine rotate_120_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-2*M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m120_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( 2*M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine rotate_60_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m60_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine flip_x = Geom::Translate(-cx, -cy) * Geom::Scale (-1, 1) * Geom::Translate(cx, cy); + Geom::Affine flip_y = Geom::Translate(-cx, -cy) * Geom::Scale (1, -1) * Geom::Translate(cx, cy); + + + // Create tile with required symmetry + const double cos60 = cos(M_PI/3); + const double sin60 = sin(M_PI/3); + const double cos30 = cos(M_PI/6); + const double sin30 = sin(M_PI/6); + + switch (type) { + + case TILE_P1: + return d_s_r * rect_translate; + break; + + case TILE_P2: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + break; + + case TILE_PM: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PG: + if (j % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_CM: + if ((i + j) % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PMM: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_PMG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_PGG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_y * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_180_c * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_CMM: + if (j % 4 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else if (j % 4 == 1) { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } else if (j % 4 == 2) { + if (i % 2 == 1) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 1) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_P4: + { + Geom::Affine ori (Geom::Translate ((w + h) * pow((i/2), shiftx_exp) + dx, (h + w) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate (w/2 + h/2, -h/2 + w/2)); + Geom::Affine dia2 (Geom::Translate (-w/2 + h/2, h/2 + w/2)); + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * ori; + } else { + return d_s_r * rotate_m90_c * dia1 * ori; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_90_c * dia2 * ori; + } else { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } + } + break; + + case TILE_P4M: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 - h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 - h/2)); + if (j % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_m90_c * dia1 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_90_c * dia2 * Geom::Translate(0, h) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate(h, 0) * Geom::Translate(0, h) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_y * flip_x * Geom::Translate(w, 0) * Geom::Translate(0, h) * ori; + } + } + } + break; + + case TILE_P4G: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow(j, shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 + h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 + h/2)); + if (((i/4) + j) % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_90_c * dia2 * ori; + } else if (i % 4 == 3) { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * Geom::Translate (-h, 0) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } + } + break; + + case TILE_P3: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (w/2 + w/2 * cos60, -(w/2 * sin60))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60))); + } else { + width = h * cos (M_PI/6); + height = h; + dia1 = Geom::Affine (Geom::Translate (h/2 * cos30, -(h/2 * sin30))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, h/2)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/3) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 3 == 0) { + return d_s_r * ori; + } else if (i % 3 == 1) { + return d_s_r * rotate_m120_c * dia1 * ori; + } else if (i % 3 == 2) { + return d_s_r * rotate_120_c * dia2 * ori; + } + } + break; + + case TILE_P31M: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((i/6) + 0.5*(j%2), shiftx_exp) + dx, (w * cos30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + ori = Geom::Affine (Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (2*h - h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P3M1: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + width = 2 * h * cos (M_PI/6); + height = 2 * h; + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/6) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P6: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + Geom::Affine dia5; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/6) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (w/2, 0)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-w/2, 0)); + } else { + ori = Geom::Affine(Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (h + h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia4 = dia3 * dia1.inverse(); + dia5 = dia3 * dia2.inverse(); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * rotate_m60_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * rotate_180_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * rotate_60_c * dia5 * ori; + } + } + break; + + case TILE_P6M: + { + + Geom::Affine ori; + Geom::Affine dia1, dia2, dia3, dia4, dia5, dia6, dia7, dia8, dia9, dia10; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/12) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2, h/2) * Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-h/2 * cos30, h/2 * sin30) * Geom::Translate (w * cos60, 0) * Geom::Translate (-h/2 * cos30, -h/2 * sin30)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-h/2 * cos30, -h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60) * Geom::Translate (w/2, -h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } else { + ori = Geom::Affine(Geom::Translate (4*h * cos30 * pow((i/12 + 0.5*(j%2)), shiftx_exp) + dx, (2*h + 2*h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h * cos30, 0) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } + if (i % 12 == 0) { + return d_s_r * ori; + } else if (i % 12 == 1) { + return d_s_r * flip_y * rotate_m60_c * dia1 * ori; + } else if (i % 12 == 2) { + return d_s_r * rotate_m60_c * dia2 * ori; + } else if (i % 12 == 3) { + return d_s_r * flip_y * rotate_m120_c * dia3 * ori; + } else if (i % 12 == 4) { + return d_s_r * rotate_m120_c * dia4 * ori; + } else if (i % 12 == 5) { + return d_s_r * flip_x * dia5 * ori; + } else if (i % 12 == 6) { + return d_s_r * flip_x * flip_y * dia6 * ori; + } else if (i % 12 == 7) { + return d_s_r * flip_y * rotate_120_c * dia7 * ori; + } else if (i % 12 == 8) { + return d_s_r * rotate_120_c * dia8 * ori; + } else if (i % 12 == 9) { + return d_s_r * flip_y * rotate_60_c * dia9 * ori; + } else if (i % 12 == 10) { + return d_s_r * rotate_60_c * dia10 * ori; + } else if (i % 12 == 11) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } + } + break; + + default: + break; + } + + return Geom::identity(); +} + +bool CloneTiler::is_a_clone_of(SPObject *tile, SPObject *obj) +{ + bool result = false; + char *id_href = nullptr; + + if (obj) { + Inkscape::XML::Node *obj_repr = obj->getRepr(); + id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + } + + if (dynamic_cast(tile) && + tile->getRepr()->attribute("xlink:href") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("xlink:href"))) && + tile->getRepr()->attribute("inkscape:tiled-clone-of") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of")))) + { + result = true; + } else { + result = false; + } + if (id_href) { + g_free(id_href); + id_href = nullptr; + } + return result; +} + +void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from) +{ + if (!trace_drawing) + return; + + for (auto& o: from->children) { + SPItem *item = dynamic_cast(&o); + if (item && is_a_clone_of(&o, nullptr)) { + item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too! + } + trace_hide_tiled_clones_recursively (&o); + } +} + +void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original) +{ + trace_drawing = new Inkscape::Drawing(); + /* Create ArenaItem and set transform */ + trace_visionkey = SPItem::display_key_new(1); + trace_doc = doc; + trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY)); + + // hide the (current) original and any tiled clones, we only want to pick the background + original->invoke_hide(trace_visionkey); + trace_hide_tiled_clones_recursively(trace_doc->getRoot()); + + trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + trace_doc->ensureUpToDate(); + + trace_zoom = zoom; +} + +guint32 CloneTiler::trace_pick(Geom::Rect box) +{ + if (!trace_drawing) { + return 0; + } + + trace_drawing->root()->setTransform(Geom::Scale(trace_zoom)); + trace_drawing->update(); + + /* Item integer bbox in points */ + Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards(); + + /* Find visible area */ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); + Inkscape::DrawingContext dc(s, ibox.min()); + /* Render */ + trace_drawing->render(dc, ibox); + double R = 0, G = 0, B = 0, A = 0; + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + + return SP_RGBA32_F_COMPOSE (R, G, B, A); +} + +void CloneTiler::trace_finish() +{ + if (trace_doc) { + trace_doc->getRoot()->invoke_hide(trace_visionkey); + delete trace_drawing; + trace_doc = nullptr; + trace_drawing = nullptr; + } +} + +void CloneTiler::unclump() +{ + auto desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + auto selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select one object whose tiled clones to unclump.")); + return; + } + + auto obj = selection->singleItem(); + auto parent = obj->parent; + + std::vector to_unclump; // not including the original + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_unclump.push_back((SPItem*)&child); + } + } + + desktop->getDocument()->ensureUpToDate(); + reverse(to_unclump.begin(),to_unclump.end()); + ::unclump (to_unclump); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Unclump tiled clones")); +} + +guint CloneTiler::number_of_clones(SPObject *obj) +{ + SPObject *parent = obj->parent; + + guint n = 0; + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + n ++; + } + } + + return n; +} + +void CloneTiler::remove(bool do_undo/* = true*/) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + Inkscape::Selection *selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select one object whose tiled clones to remove.")); + return; + } + + SPObject *obj = selection->singleItem(); + SPObject *parent = obj->parent; + +// remove old tiling + std::vector to_delete; + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_delete.push_back(&child); + } + } + for (auto obj:to_delete) { + g_assert(obj != nullptr); + obj->deleteObject(); + } + + change_selection (selection); + + if (do_undo) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Delete tiled clones")); + } +} + +Geom::Rect CloneTiler::transform_rect(Geom::Rect const &r, Geom::Affine const &m) +{ + using Geom::X; + using Geom::Y; + Geom::Point const p1 = r.corner(1) * m; + Geom::Point const p2 = r.corner(2) * m; + Geom::Point const p3 = r.corner(3) * m; + Geom::Point const p4 = r.corner(4) * m; + return Geom::Rect( + Geom::Point( + std::min(std::min(p1[X], p2[X]), std::min(p3[X], p4[X])), + std::min(std::min(p1[Y], p2[Y]), std::min(p3[Y], p4[Y]))), + Geom::Point( + std::max(std::max(p1[X], p2[X]), std::max(p3[X], p4[X])), + std::max(std::max(p1[Y], p2[Y]), std::max(p3[Y], p4[Y])))); +} + +/** +Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same +probability of being displaced. + */ +double CloneTiler::randomize01(double val, double rand) +{ + double base = MIN (val - rand, 1 - 2*rand); + if (base < 0) { + base = 0; + } + val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); + return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... +} + + +void CloneTiler::apply() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Selection *selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an object to clone.")); + return; + } + + // Check if more than one object is selected. + if (boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("If you want to clone several objects, group them and clone the group.")); + return; + } + + // set "busy" cursor + desktop->setWaitingCursor(); + + // set statusbar text + gtk_label_set_markup (GTK_LABEL(_status), _("Creating tiled clones...")); + gtk_widget_queue_draw(GTK_WIDGET(_status)); + + SPObject *obj = selection->singleItem(); + if (!obj) { + // Should never happen (empty selection checked above). + std::cerr << "CloneTiler::clonetile_apply(): No object in single item selection!!!" << std::endl; + return; + } + Inkscape::XML::Node *obj_repr = obj->getRepr(); + const char *id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + SPObject *parent = obj->parent; + + remove(false); + + Geom::Scale scale = desktop->getDocument()->getDocumentScale().inverse(); + double scale_units = scale[Geom::X]; // Use just x direction.... + + double shiftx_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_i", 0, -10000, 10000); + double shifty_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_i", 0, -10000, 10000); + double shiftx_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_j", 0, -10000, 10000); + double shifty_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_j", 0, -10000, 10000); + double shiftx_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_rand", 0, 0, 1000); + double shifty_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_rand", 0, 0, 1000); + double shiftx_exp = prefs->getDoubleLimited(prefs_path + "shiftx_exp", 1, 0, 10); + double shifty_exp = prefs->getDoubleLimited(prefs_path + "shifty_exp", 1, 0, 10); + bool shiftx_alternate = prefs->getBool(prefs_path + "shiftx_alternate"); + bool shifty_alternate = prefs->getBool(prefs_path + "shifty_alternate"); + bool shiftx_cumulate = prefs->getBool(prefs_path + "shiftx_cumulate"); + bool shifty_cumulate = prefs->getBool(prefs_path + "shifty_cumulate"); + bool shiftx_excludew = prefs->getBool(prefs_path + "shiftx_excludew"); + bool shifty_excludeh = prefs->getBool(prefs_path + "shifty_excludeh"); + + double scalex_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_i", 0, -100, 1000); + double scaley_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_i", 0, -100, 1000); + double scalex_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_j", 0, -100, 1000); + double scaley_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_j", 0, -100, 1000); + double scalex_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_rand", 0, 0, 1000); + double scaley_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_rand", 0, 0, 1000); + double scalex_exp = prefs->getDoubleLimited(prefs_path + "scalex_exp", 1, 0, 10); + double scaley_exp = prefs->getDoubleLimited(prefs_path + "scaley_exp", 1, 0, 10); + double scalex_log = prefs->getDoubleLimited(prefs_path + "scalex_log", 0, 0, 10); + double scaley_log = prefs->getDoubleLimited(prefs_path + "scaley_log", 0, 0, 10); + bool scalex_alternate = prefs->getBool(prefs_path + "scalex_alternate"); + bool scaley_alternate = prefs->getBool(prefs_path + "scaley_alternate"); + bool scalex_cumulate = prefs->getBool(prefs_path + "scalex_cumulate"); + bool scaley_cumulate = prefs->getBool(prefs_path + "scaley_cumulate"); + + double rotate_per_i = prefs->getDoubleLimited(prefs_path + "rotate_per_i", 0, -180, 180); + double rotate_per_j = prefs->getDoubleLimited(prefs_path + "rotate_per_j", 0, -180, 180); + double rotate_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "rotate_rand", 0, 0, 100); + bool rotate_alternatei = prefs->getBool(prefs_path + "rotate_alternatei"); + bool rotate_alternatej = prefs->getBool(prefs_path + "rotate_alternatej"); + bool rotate_cumulatei = prefs->getBool(prefs_path + "rotate_cumulatei"); + bool rotate_cumulatej = prefs->getBool(prefs_path + "rotate_cumulatej"); + + double blur_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_i", 0, 0, 100); + double blur_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_j", 0, 0, 100); + bool blur_alternatei = prefs->getBool(prefs_path + "blur_alternatei"); + bool blur_alternatej = prefs->getBool(prefs_path + "blur_alternatej"); + double blur_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_rand", 0, 0, 100); + + double opacity_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_i", 0, 0, 100); + double opacity_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_j", 0, 0, 100); + bool opacity_alternatei = prefs->getBool(prefs_path + "opacity_alternatei"); + bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej"); + double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100); + + Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color"); + double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100); + double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100); + double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100); + double saturation_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_j", 0, -100, 100); + double saturation_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_i", 0, -100, 100); + double saturation_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_rand", 0, 0, 100); + double lightness_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_j", 0, -100, 100); + double lightness_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_i", 0, -100, 100); + double lightness_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_rand", 0, 0, 100); + bool color_alternatej = prefs->getBool(prefs_path + "color_alternatej"); + bool color_alternatei = prefs->getBool(prefs_path + "color_alternatei"); + + int type = prefs->getInt(prefs_path + "symmetrygroup", 0); + bool keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + int imax = prefs->getInt(prefs_path + "imax", 2); + int jmax = prefs->getInt(prefs_path + "jmax", 2); + + bool fillrect = prefs->getBool(prefs_path + "fillrect"); + double fillwidth = scale_units*prefs->getDoubleLimited(prefs_path + "fillwidth", 50, 0, 1e6); + double fillheight = scale_units*prefs->getDoubleLimited(prefs_path + "fillheight", 50, 0, 1e6); + + bool dotrace = prefs->getBool(prefs_path + "dotrace"); + int pick = prefs->getInt(prefs_path + "pick"); + bool pick_to_presence = prefs->getBool(prefs_path + "pick_to_presence"); + bool pick_to_size = prefs->getBool(prefs_path + "pick_to_size"); + bool pick_to_color = prefs->getBool(prefs_path + "pick_to_color"); + bool pick_to_opacity = prefs->getBool(prefs_path + "pick_to_opacity"); + double rand_picked = 0.01 * prefs->getDoubleLimited(prefs_path + "rand_picked", 0, 0, 100); + bool invert_picked = prefs->getBool(prefs_path + "invert_picked"); + double gamma_picked = prefs->getDoubleLimited(prefs_path + "gamma_picked", 0, -10, 10); + + SPItem *item = dynamic_cast(obj); + if (dotrace) { + trace_setup(desktop->getDocument(), 1.0, item); + } + + Geom::Point center; + double w = 0; + double h = 0; + double x0 = 0; + double y0 = 0; + + if (keepbbox && + obj_repr->attribute("inkscape:tile-w") && + obj_repr->attribute("inkscape:tile-h") && + obj_repr->attribute("inkscape:tile-x0") && + obj_repr->attribute("inkscape:tile-y0") && + obj_repr->attribute("inkscape:tile-cx") && + obj_repr->attribute("inkscape:tile-cy")) { + + double cx = 0; + double cy = 0; + sp_repr_get_double (obj_repr, "inkscape:tile-cx", &cx); + sp_repr_get_double (obj_repr, "inkscape:tile-cy", &cy); + center = Geom::Point (cx, cy); + + sp_repr_get_double (obj_repr, "inkscape:tile-w", &w); + sp_repr_get_double (obj_repr, "inkscape:tile-h", &h); + sp_repr_get_double (obj_repr, "inkscape:tile-x0", &x0); + sp_repr_get_double (obj_repr, "inkscape:tile-y0", &y0); + } else { + bool prefs_bbox = prefs->getBool("/tools/bounding_box", false); + SPItem::BBoxType bbox_type = ( !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX ); + Geom::OptRect r = item->documentBounds(bbox_type); + if (r) { + w = scale_units*r->dimensions()[Geom::X]; + h = scale_units*r->dimensions()[Geom::Y]; + x0 = scale_units*r->min()[Geom::X]; + y0 = scale_units*r->min()[Geom::Y]; + center = scale_units*desktop->dt2doc(item->getCenter()); + + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cx", center[Geom::X]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cy", center[Geom::Y]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-w", w); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-h", h); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-x0", x0); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-y0", y0); + } else { + center = Geom::Point(0, 0); + w = h = 0; + x0 = y0 = 0; + } + } + + Geom::Point cur(0, 0); + Geom::Rect bbox_original (Geom::Point (x0, y0), Geom::Point (x0 + w, y0 + h)); + double perimeter_original = (w + h)/4; + + // The integers i and j are reserved for tile column and row. + // The doubles x and y are used for coordinates + for (int i = 0; + fillrect? + (fabs(cur[Geom::X]) < fillwidth && i < 200) // prevent "freezing" with too large fillrect, arbitrarily limit rows + : (i < imax); + i ++) { + for (int j = 0; + fillrect? + (fabs(cur[Geom::Y]) < fillheight && j < 200) // prevent "freezing" with too large fillrect, arbitrarily limit cols + : (j < jmax); + j ++) { + + // Note: We create a clone at 0,0 too, right over the original, in case our clones are colored + + // Get transform from symmetry, shift, scale, rotation + Geom::Affine orig_t = get_transform (type, i, j, center[Geom::X], center[Geom::Y], w, h, + shiftx_per_i, shifty_per_i, + shiftx_per_j, shifty_per_j, + shiftx_rand, shifty_rand, + shiftx_exp, shifty_exp, + shiftx_alternate, shifty_alternate, + shiftx_cumulate, shifty_cumulate, + shiftx_excludew, shifty_excludeh, + scalex_per_i, scaley_per_i, + scalex_per_j, scaley_per_j, + scalex_rand, scaley_rand, + scalex_exp, scaley_exp, + scalex_log, scaley_log, + scalex_alternate, scaley_alternate, + scalex_cumulate, scaley_cumulate, + rotate_per_i, rotate_per_j, + rotate_rand, + rotate_alternatei, rotate_alternatej, + rotate_cumulatei, rotate_cumulatej ); + Geom::Affine parent_transform = (((SPItem*)item->parent)->i2doc_affine())*(item->document->getRoot()->c2p.inverse()); + Geom::Affine t = parent_transform*orig_t*parent_transform.inverse(); + cur = center * t - center; + if (fillrect) { + if ((cur[Geom::X] > fillwidth) || (cur[Geom::Y] > fillheight)) { // off limits + continue; + } + } + + gchar color_string[32]; *color_string = 0; + + // Color tab + if (!initial_color.empty()) { + guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + double eff_i = (color_alternatei? (i%2) : (i)); + double eff_j = (color_alternatej? (j%2) : (j)); + + hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1); + double notused; + hsl[0] = modf( hsl[0], ¬used ); // Restrict to 0-1 + hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1); + hsl[1] = CLAMP (hsl[1], 0, 1); + hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1); + hsl[2] = CLAMP (hsl[2], 0, 1); + + float rgb[3]; + SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]); + sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); + } + + // Blur + double blur = 0.0; + { + int eff_i = (blur_alternatei? (i%2) : (i)); + int eff_j = (blur_alternatej? (j%2) : (j)); + blur = (blur_per_i * eff_i + blur_per_j * eff_j + blur_rand * g_random_double_range (-1, 1)); + blur = CLAMP (blur, 0, 1); + } + + // Opacity + double opacity = 1.0; + { + int eff_i = (opacity_alternatei? (i%2) : (i)); + int eff_j = (opacity_alternatej? (j%2) : (j)); + opacity = 1 - (opacity_per_i * eff_i + opacity_per_j * eff_j + opacity_rand * g_random_double_range (-1, 1)); + opacity = CLAMP (opacity, 0, 1); + } + + // Trace tab + if (dotrace) { + Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units)); + + guint32 rgba = trace_pick (bbox_t); + float r = SP_RGBA32_R_F(rgba); + float g = SP_RGBA32_G_F(rgba); + float b = SP_RGBA32_B_F(rgba); + float a = SP_RGBA32_A_F(rgba); + + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + + gdouble val = 0; + switch (pick) { + case PICK_COLOR: + val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max + break; + case PICK_OPACITY: + val = a; + break; + case PICK_R: + val = r; + break; + case PICK_G: + val = g; + break; + case PICK_B: + val = b; + break; + case PICK_H: + val = hsl[0]; + break; + case PICK_S: + val = hsl[1]; + break; + case PICK_L: + val = 1 - hsl[2]; + break; + default: + break; + } + + if (rand_picked > 0) { + val = randomize01 (val, rand_picked); + r = randomize01 (r, rand_picked); + g = randomize01 (g, rand_picked); + b = randomize01 (b, rand_picked); + } + + if (gamma_picked != 0) { + double power; + if (gamma_picked > 0) + power = 1/(1 + fabs(gamma_picked)); + else + power = 1 + fabs(gamma_picked); + + val = pow (val, power); + r = pow (r, power); + g = pow (g, power); + b = pow (b, power); + } + + if (invert_picked) { + val = 1 - val; + r = 1 - r; + g = 1 - g; + b = 1 - b; + } + + val = CLAMP (val, 0, 1); + r = CLAMP (r, 0, 1); + g = CLAMP (g, 0, 1); + b = CLAMP (b, 0, 1); + + // recompose tweaked color + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + + if (pick_to_presence) { + if (g_random_double_range (0, 1) > val) { + continue; // skip! + } + } + if (pick_to_size) { + t = parent_transform * Geom::Translate(-center[Geom::X], -center[Geom::Y]) + * Geom::Scale (val, val) * Geom::Translate(center[Geom::X], center[Geom::Y]) + * parent_transform.inverse() * t; + } + if (pick_to_opacity) { + opacity *= val; + } + if (pick_to_color) { + sp_svg_write_color(color_string, sizeof(color_string), rgba); + } + } + + if (opacity < 1e-6) { // invisibly transparent, skip + continue; + } + + if (fabs(t[0]) + fabs (t[1]) + fabs(t[2]) + fabs(t[3]) < 1e-6) { // too small, skip + continue; + } + + // Create the clone + Inkscape::XML::Node *clone = obj_repr->document()->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + clone->setAttribute("inkscape:tiled-clone-of", id_href); + clone->setAttribute("xlink:href", id_href); + + Geom::Point new_center; + bool center_set = false; + if (obj_repr->attribute("inkscape:transform-center-x") || obj_repr->attribute("inkscape:transform-center-y")) { + new_center = scale_units*desktop->dt2doc(item->getCenter()) * orig_t; + center_set = true; + } + + gchar *affinestr=sp_svg_transform_write(t); + clone->setAttribute("transform", affinestr); + g_free(affinestr); + + if (opacity < 1.0) { + sp_repr_set_css_double(clone, "opacity", opacity); + } + + if (*color_string) { + clone->setAttribute("fill", color_string); + clone->setAttribute("stroke", color_string); + } + + // add the new clone to the top of the original's parent + parent->getRepr()->appendChild(clone); + + if (blur > 0.0) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + double perimeter = perimeter_original * t.descrim(); + double radius = blur * perimeter; + // this is necessary for all newly added clones to have correct bboxes, + // otherwise filters won't work: + desktop->getDocument()->ensureUpToDate(); + // it's hard to figure out exact width/height of the tile without having an object + // that we can take bbox of; however here we only need a lower bound so that blur + // margins are not too small, and the perimeter should work + SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim(), t.expansionX(), t.expansionY(), perimeter, perimeter); + sp_style_set_property_url (clone_object, "filter", constructed, false); + } + + if (center_set) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + SPItem *item = dynamic_cast(clone_object); + if (clone_object && item) { + clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + item->setCenter(desktop->doc2dt(new_center)); + clone_object->updateRepr(); + } + } + + Inkscape::GC::release(clone); + } + cur[Geom::Y] = 0; + } + + if (dotrace) { + trace_finish (); + } + + change_selection(selection); + + desktop->clearWaitingCursor(); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Create tiled clones")); +} + +GtkWidget * CloneTiler::new_tab(GtkWidget *nb, const gchar *label) +{ + GtkWidget *l = gtk_label_new_with_mnemonic (label); + auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (vb), VB_MARGIN); + gtk_notebook_append_page (GTK_NOTEBOOK (nb), vb, l); + return vb; +} + +void CloneTiler::checkbox_toggled(Gtk::ToggleButton *tb, + const Glib::ustring &attr) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + attr, tb->get_active()); +} + +Gtk::Widget * CloneTiler::checkbox(const char *tip, + const Glib::ustring &attr) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + auto b = Gtk::manage(new Gtk::CheckButton()); + b->set_tooltip_text(tip); + + auto const prefs = Inkscape::Preferences::get(); + auto const value = prefs->getBool(prefs_path + attr); + b->set_active(value); + + hb->pack_start(*b, false, true); + b->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::checkbox_toggled), b, attr)); + + b->set_data("uncheckable", GINT_TO_POINTER(true)); + + return hb; +} + +void CloneTiler::value_changed(Glib::RefPtr &adj, + Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + pref, adj->get_value()); +} + +Gtk::Widget * CloneTiler::spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent/* = false*/) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + + { + // Parameters for adjustment + auto const initial_value = (exponent ? 1.0 : 0.0); + auto const step_increment = (exponent ? 0.01 : 0.1); + auto const page_increment = (exponent ? 0.05 : 0.4); + + auto a = Gtk::Adjustment::create(initial_value, + lower, + upper, + step_increment, + page_increment); + + auto const climb_rate = (exponent ? 0.01 : 0.1); + auto const digits = (exponent ? 2 : 1); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, climb_rate, digits); + + sb->set_tooltip_text (tip); + sb->set_width_chars (5); + sb->set_digits(3); + hb->pack_start(*sb, false, false, SB_MARGIN); + + auto prefs = Inkscape::Preferences::get(); + auto value = prefs->getDoubleLimited(prefs_path + attr, exponent? 1.0 : 0.0, lower, upper); + a->set_value (value); + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::value_changed), a, attr)); + + if (exponent) { + sb->set_data ("oneable", GINT_TO_POINTER(TRUE)); + } else { + sb->set_data ("zeroable", GINT_TO_POINTER(TRUE)); + } + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(suffix); + hb->pack_start(*l); + } + + return hb; +} + +void CloneTiler::symgroup_changed(Gtk::ComboBox *cb) +{ + auto prefs = Inkscape::Preferences::get(); + auto group_new = cb->get_active_row_number(); + prefs->setInt(prefs_path + "symmetrygroup", group_new); +} + +void CloneTiler::xy_changed(Glib::RefPtr &adj, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + pref, (int) floor(adj->get_value() + 0.5)); +} + +void CloneTiler::keep_bbox_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "keepbbox", _cb_keep_bbox->get_active()); +} + +void CloneTiler::pick_to(Gtk::ToggleButton *tb, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + pref, tb->get_active()); +} + + +void CloneTiler::reset_recursive(GtkWidget *w) +{ + if (w && G_IS_OBJECT(w)) { + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "zeroable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 0); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "oneable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 1); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "uncheckable")); + if (r && GTK_IS_TOGGLE_BUTTON(w)) { // checkbox + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(w), FALSE); + } + } + } + + if (GTK_IS_CONTAINER(w)) { + std::vector c = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for ( auto i : c ) { + reset_recursive(i->gobj()); + } + } +} + +void CloneTiler::reset() +{ + reset_recursive(GTK_WIDGET(this->gobj())); +} + +void CloneTiler::table_attach(GtkWidget *table, Gtk::Widget *widget, float align, int row, int col) +{ + table_attach(table, GTK_WIDGET(widget->gobj()), align, row, col); +} + +void CloneTiler::table_attach(GtkWidget *table, GtkWidget *widget, float align, int row, int col) +{ + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(table), widget, col, row, 1, 1); +} + +GtkWidget * CloneTiler::table_x_y_rand(int values) +{ + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 6); + gtk_grid_set_column_spacing(GTK_GRID(table), 8); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("Per row:")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 2); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("Per column:")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 3); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Randomize:")); + table_attach (table, l, 0, 1, 4); + } + + return table; +} + +void CloneTiler::pick_switched(PickType v) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + "pick", v); +} + +void CloneTiler::switch_to_create() +{ + if (_rowscols) { + _rowscols->set_sensitive(true); + } + if (_widthheight) { + _widthheight->set_sensitive(false); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", false); +} + + +void CloneTiler::switch_to_fill() +{ + if (_rowscols) { + _rowscols->set_sensitive(false); + } + if (_widthheight) { + _widthheight->set_sensitive(true); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", true); +} + +void CloneTiler::fill_width_changed() +{ + auto const raw_dist = fill_width->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillwidth", pixels); +} + +void CloneTiler::fill_height_changed() +{ + auto const raw_dist = fill_height->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillheight", pixels); +} + +void CloneTiler::unit_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble width_pixels = prefs->getDouble(prefs_path + "fillwidth"); + gdouble height_pixels = prefs->getDouble(prefs_path + "fillheight"); + + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + + gdouble width_value = Inkscape::Util::Quantity::convert(width_pixels, "px", unit); + gdouble height_value = Inkscape::Util::Quantity::convert(height_pixels, "px", unit); + gtk_adjustment_set_value(fill_width->gobj(), width_value); + gtk_adjustment_set_value(fill_height->gobj(), height_value); +} + +void CloneTiler::do_pick_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + auto active = _b->get_active(); + prefs->setBool(prefs_path + "dotrace", active); + + if (_dotrace) { + gtk_widget_set_sensitive (_dotrace, active); + } +} + +void CloneTiler::show_page_trace() +{ + gtk_notebook_set_current_page(GTK_NOTEBOOK(nb),6); + _b->set_active(false); +} + + +} +} +} + + +/* + 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 : diff --git a/src/ui/dialog/clonetiler.h b/src/ui/dialog/clonetiler.h new file mode 100644 index 0000000..47c20ad --- /dev/null +++ b/src/ui/dialog/clonetiler.h @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Clone tiling dialog + */ +/* Authors: + * bulia byak + * + * Copyright (C) 2004 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __SP_CLONE_TILER_H__ +#define __SP_CLONE_TILER_H__ + +#include "ui/widget/panel.h" + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/color-picker.h" + +namespace Gtk { + class CheckButton; + class ComboBox; + class ToggleButton; +} + +class SPItem; +class SPObject; + +namespace Geom { + class Rect; + class Affine; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +} + +namespace Dialog { + +class CloneTiler : public Widget::Panel { +public: + CloneTiler(); + ~CloneTiler() override; + + static CloneTiler &getInstance() { return *new CloneTiler(); } + void show_page_trace(); +protected: + enum PickType { + PICK_COLOR, + PICK_OPACITY, + PICK_R, + PICK_G, + PICK_B, + PICK_H, + PICK_S, + PICK_L + }; + + GtkWidget * new_tab(GtkWidget *nb, const gchar *label); + GtkWidget * table_x_y_rand(int values); + Gtk::Widget * spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent = false); + Gtk::Widget * checkbox(const char *tip, + const Glib::ustring &attr); + void table_attach(GtkWidget *table, GtkWidget *widget, float align, int row, int col); + void table_attach(GtkWidget *table, Gtk::Widget *widget, float align, int row, int col); + + void symgroup_changed(Gtk::ComboBox *cb); + void on_picker_color_changed(guint rgba); + void trace_hide_tiled_clones_recursively(SPObject *from); + guint number_of_clones(SPObject *obj); + void trace_setup(SPDocument *doc, gdouble zoom, SPItem *original); + guint32 trace_pick(Geom::Rect box); + void trace_finish(); + bool is_a_clone_of(SPObject *tile, SPObject *obj); + Geom::Rect transform_rect(Geom::Rect const &r, Geom::Affine const &m); + double randomize01(double val, double rand); + + void apply(); + void change_selection(Inkscape::Selection *selection); + void checkbox_toggled(Gtk::ToggleButton *tb, + Glib::ustring const &attr); + void do_pick_toggled(); + void external_change(); + void fill_width_changed(); + void fill_height_changed(); + void keep_bbox_toggled(); + void on_remove_button_clicked() {remove();} + void pick_switched(PickType); + void pick_to(Gtk::ToggleButton *tb, + Glib::ustring const &pref); + void remove(bool do_undo = true); + void reset(); + void reset_recursive(GtkWidget *w); + void switch_to_create(); + void switch_to_fill(); + void unclump(); + void unit_changed(); + void value_changed(Glib::RefPtr &adj, Glib::ustring const &pref); + void xy_changed(Glib::RefPtr &adj, Glib::ustring const &pref); + + Geom::Affine get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ); + + +private: + CloneTiler(CloneTiler const &d) = delete; + CloneTiler& operator=(CloneTiler const &d) = delete; + + Gtk::CheckButton *_b; + Gtk::CheckButton *_cb_keep_bbox; + GtkWidget *nb; + SPDesktop *desktop; + DesktopTracker deskTrack; + Inkscape::UI::Widget::ColorPicker *color_picker; + GtkSizeGroup* table_row_labels; + Inkscape::UI::Widget::UnitMenu *unit_menu; + + Glib::RefPtr fill_width; + Glib::RefPtr fill_height; + + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection externChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection color_changed_connection; + sigc::connection unitChangedConn; + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + // Variables that used to be set using GObject + GtkWidget *_buttons_on_tiles; + GtkWidget *_dotrace; + GtkWidget *_status; + Gtk::Box *_rowscols; + Gtk::Box *_widthheight; + +}; + + +enum { + TILE_P1, + TILE_P2, + TILE_PM, + TILE_PG, + TILE_CM, + TILE_PMM, + TILE_PMG, + TILE_PGG, + TILE_CMM, + TILE_P4, + TILE_P4M, + TILE_P4G, + TILE_P3, + TILE_P31M, + TILE_P3M1, + TILE_P6, + TILE_P6M +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif + +/* + 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 : diff --git a/src/ui/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp new file mode 100644 index 0000000..acec954 --- /dev/null +++ b/src/ui/dialog/color-item.cpp @@ -0,0 +1,751 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include + +#include "color-item.h" + +#include "desktop.h" + +#include "desktop-style.h" +#include "display/cairo-utils.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" // for SP_ACTIVE_DESKTOP +#include "io/resource.h" +#include "io/sys.h" +#include "message-context.h" +#include "svg/svg-color.h" +#include "verbs.h" +#include "widgets/gradient-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static std::vector mimeStrings; +static std::map mimeToInt; + + +#if ENABLE_MAGIC_COLORS +// TODO remove this soon: +extern std::vector possible; +#endif // ENABLE_MAGIC_COLORS + + +#if ENABLE_MAGIC_COLORS +static bool bruteForce( SPDocument* document, Inkscape::XML::Node* node, Glib::ustring const& match, int r, int g, int b ) +{ + bool changed = false; + + if ( node ) { + gchar const * val = node->attribute("inkscape:x-fill-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "fill", c ); + + sp_desktop_apply_css_recursive( obj, css, true ); + static_cast(obj)->updateRepr(); + + changed = true; + } + + val = node->attribute("inkscape:x-stroke-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "stroke", c ); + + sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); + ((SPItem*)obj)->updateRepr(); + + changed = true; + } + + Inkscape::XML::Node* first = node->firstChild(); + changed |= bruteForce( document, first, match, r, g, b ); + + changed |= bruteForce( document, node->next(), match, r, g, b ); + } + + return changed; +} +#endif // ENABLE_MAGIC_COLORS + +void +ColorItem::handleClick() { + buttonClicked(false); +} + +void +ColorItem::handleSecondaryClick(gint /*arg1*/) { + buttonClicked(true); +} + +bool +ColorItem::handleEnterNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + gchar* msg = g_strdup_printf(_("Color: %s; Click to set fill, Shift+click to set stroke"), + def.descr.c_str()); + desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg); + g_free(msg); + } + + return false; +} + +bool +ColorItem::handleLeaveNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if ( desktop ) { + desktop->tipsMessageContext()->clear(); + } + + return false; +} + +static void dieDieDie( GObject *obj, gpointer user_data ) +{ + g_message("die die die %p %p", obj, user_data ); +} + +static bool getBlock( std::string& dst, guchar ch, std::string const & str ) +{ + bool good = false; + std::string::size_type pos = str.find(ch); + if ( pos != std::string::npos ) + { + std::string::size_type pos2 = str.find( '(', pos ); + if ( pos2 != std::string::npos ) { + std::string::size_type endPos = str.find( ')', pos2 ); + if ( endPos != std::string::npos ) { + dst = str.substr( pos2 + 1, (endPos - pos2 - 1) ); + good = true; + } + } + } + return good; +} + +static bool popVal( guint64& numVal, std::string& str ) +{ + bool good = false; + std::string::size_type endPos = str.find(','); + if ( endPos == std::string::npos ) { + endPos = str.length(); + } + + if ( endPos != std::string::npos && endPos > 0 ) { + std::string xxx = str.substr( 0, endPos ); + const gchar* ptr = xxx.c_str(); + gchar* endPtr = nullptr; + numVal = g_ascii_strtoull( ptr, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == ptr) ) { + // failed conversion + } else { + good = true; + str.erase( 0, endPos + 1 ); + } + } + + return good; +} + +// TODO resolve this more cleanly: +extern bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data); + +void +ColorItem::drag_begin(const Glib::RefPtr &dc) +{ + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + int width = 32; + int height = 24; + + if (def.getType() != ege::PaintDef::RGB){ + GError *error; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, + &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename, width, height, false); + g_free(localFilename); + dc->set_icon(pixbuf, 0, 0); + } else { + Glib::RefPtr pixbuf; + if (getGradient() ){ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_pattern_t *gradient = getGradient()->create_preview_pattern(width); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, gradient); + cairo_paint(ct); + cairo_destroy(ct); + cairo_pattern_destroy(gradient); + cairo_surface_flush(s); + + pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + } else { + pixbuf = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height ); + guint32 fillWith = (0xff000000 & (def.getR() << 24)) + | (0x00ff0000 & (def.getG() << 16)) + | (0x0000ff00 & (def.getB() << 8)); + pixbuf->fill( fillWith ); + } + dc->set_icon(pixbuf, 0, 0); + } +} + +//"drag-drop" +// gboolean dragDropColorData( GtkWidget *widget, +// GdkDragContext *drag_context, +// gint x, +// gint y, +// guint time, +// gpointer user_data) +// { +// // TODO finish + +// return TRUE; +// } + + +SwatchPage::SwatchPage() + : _prefWidth(0) +{ +} + +SwatchPage::~SwatchPage() += default; + + +ColorItem::ColorItem(ege::PaintDef::ColorType type) : + def(type), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) : + def( r, g, b, name ), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::~ColorItem() +{ + if (_pattern != nullptr) { + cairo_pattern_destroy(_pattern); + } +} + +ColorItem::ColorItem(ColorItem const &other) : + Inkscape::UI::Previewable() +{ + if ( this != &other ) { + *this = other; + } +} + +ColorItem &ColorItem::operator=(ColorItem const &other) +{ + if ( this != &other ) { + def = other.def; + + // TODO - correct linkage + _linkSrc = other._linkSrc; + g_message("Erk!"); + } + return *this; +} + +void ColorItem::setState( bool fill, bool stroke ) +{ + if ( (_isFill != fill) || (_isStroke != stroke) ) { + _isFill = fill; + _isStroke = stroke; + + for ( auto widget : _previews ) { + auto preview = dynamic_cast(widget); + + if (preview) { + int val = preview->get_linked(); + val &= ~(UI::Widget::PREVIEW_FILL | UI::Widget::PREVIEW_STROKE); + if ( _isFill ) { + val |= UI::Widget::PREVIEW_FILL; + } + if ( _isStroke ) { + val |= UI::Widget::PREVIEW_STROKE; + } + preview->set_linked(static_cast(val)); + } + } + } +} + +void ColorItem::setGradient(SPGradient *grad) +{ + if (_grad != grad) { + _grad = grad; + // TODO regen and push to listeners + } + + setName( gr_prepare_label(_grad) ); +} + +void ColorItem::setName(const Glib::ustring name) +{ + //def.descr = name; + + for (auto widget : _previews) { + auto preview = dynamic_cast(widget); + auto label = dynamic_cast(widget); + if (preview) { + preview->set_tooltip_text(name); + } + else if (label) { + label->set_text(name); + } + } +} + +void ColorItem::setPattern(cairo_pattern_t *pattern) +{ + if (pattern) { + cairo_pattern_reference(pattern); + } + if (_pattern) { + cairo_pattern_destroy(_pattern); + } + _pattern = pattern; + + _updatePreviews(); +} + +void +ColorItem::_dragGetColorData(const Glib::RefPtr& /*drag_context*/, + Gtk::SelectionData &data, + guint info, + guint /*time*/) +{ + std::string key; + if ( info < mimeStrings.size() ) { + key = mimeStrings[info]; + } else { + g_warning("ERROR: unknown value (%d)", info); + } + + if ( !key.empty() ) { + char* tmp = nullptr; + int len = 0; + int format = 0; + def.getMIMEData(key, tmp, len, format); + if ( tmp ) { + data.set(key, format, (guchar*)tmp, len ); + delete[] tmp; + } + } +} + +void ColorItem::_dropDataIn( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData */*data*/, + guint /*info*/, + guint /*event_time*/, + gpointer /*user_data*/) +{ +} + +void ColorItem::_colorDefChanged(void* data) +{ + ColorItem* item = reinterpret_cast(data); + if ( item ) { + item->_updatePreviews(); + } +} + +void ColorItem::_updatePreviews() +{ + for (auto widget : _previews) { + auto preview = dynamic_cast(widget); + if (preview) { + _regenPreview(preview); + preview->queue_draw(); + } + } + + for (auto & _listener : _listeners) { + guint r = def.getR(); + guint g = def.getG(); + guint b = def.getB(); + + if ( _listener->_linkIsTone ) { + r = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * b) ) / 100; + } else { + r = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * b) ) / 100; + } + + _listener->def.setRGB( r, g, b ); + } + + +#if ENABLE_MAGIC_COLORS + // Look for objects using this color + { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + SPDocument* document = desktop->getDocument(); + Inkscape::XML::Node *rroot = document->getReprRoot(); + if ( rroot ) { + + // Find where this thing came from + Glib::ustring paletteName; + bool found = false; + int index = 0; + for ( std::vector::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) { + SwatchPage* curr = *it2; + index = 0; + for ( boost::ptr_vector::iterator zz = curr->_colors.begin(); zz != curr->_colors.end(); ++zz ) { + if ( this == &*zz ) { + found = true; + paletteName = curr->_name; + break; + } else { + index++; + } + } + } + + if ( !paletteName.empty() ) { + gchar* str = g_strdup_printf("%d|", index); + paletteName.insert( 0, str ); + g_free(str); + str = 0; + + if ( bruteForce( document, rroot, paletteName, def.getR(), def.getG(), def.getB() ) ) { + SPDocumentUndo::done( document , SP_VERB_DIALOG_SWATCHES, + _("Change color definition")); + } + } + } + } + } +#endif // ENABLE_MAGIC_COLORS + +} + +void ColorItem::_regenPreview(UI::Widget::Preview * preview) +{ + if ( def.getType() != ege::PaintDef::RGB ) { + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + GError *error = nullptr; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = + g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename); + if (!pixbuf) { + g_warning("Null pixbuf for %p [%s]", localFilename, localFilename ); + } + g_free(localFilename); + + preview->set_pixbuf(pixbuf); + } + else if ( !_pattern ){ + preview->set_color((def.getR() << 8) | def.getR(), + (def.getG() << 8) | def.getG(), + (def.getB() << 8) | def.getB() ); + } else { + // These correspond to PREVIEW_PIXBUF_WIDTH and VBLOCK from swatches.cpp + // TODO: the pattern to draw should be in the widget that draws the preview, + // so the preview can be scalable + int w = 128; + int h = 16; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, _pattern); + cairo_paint(ct); + cairo_destroy(ct); + cairo_surface_flush(s); + + auto pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + preview->set_pixbuf(pixbuf); + } + + preview->set_linked(static_cast( (_linkSrc ? UI::Widget::PREVIEW_LINK_IN : 0) + | (_listeners.empty() ? 0 : UI::Widget::PREVIEW_LINK_OUT) + | (_isLive ? UI::Widget::PREVIEW_LINK_OTHER:0)) ); +} + +Gtk::Widget* +ColorItem::getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) +{ + Gtk::Widget* widget = nullptr; + if ( style == UI::Widget::PREVIEW_STYLE_BLURB) { + Gtk::Label *lbl = new Gtk::Label(def.descr); + lbl->set_halign(Gtk::ALIGN_START); + lbl->set_valign(Gtk::ALIGN_CENTER); + widget = lbl; + } else { + auto preview = Gtk::manage(new UI::Widget::Preview()); + preview->set_name("ColorItemPreview"); + + _regenPreview(preview); + + preview->set_details((UI::Widget::ViewType)view, + (UI::Widget::PreviewSize)size, + ratio, + border ); + + def.addCallback( _colorDefChanged, this ); + preview->set_focus_on_click(false); + preview->set_tooltip_text(def.descr); + + preview->signal_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleClick)); + preview->signal_alt_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleSecondaryClick)); + preview->signal_button_press_event().connect(sigc::bind(sigc::ptr_fun(&colorItemHandleButtonPress), preview, this)); + + { + auto listing = def.getMIMETypes(); + std::vector entries; + + for ( auto str : listing ) { + auto target = str.c_str(); + guint flags = 0; + if ( mimeToInt.find(str) == mimeToInt.end() ){ + // these next lines are order-dependent: + mimeToInt[str] = mimeStrings.size(); + mimeStrings.push_back(str); + } + auto info = mimeToInt[target]; + Gtk::TargetEntry entry(target, (Gtk::TargetFlags)flags, info); + entries.push_back(entry); + } + + preview->drag_source_set(entries, Gdk::BUTTON1_MASK, + Gdk::DragAction(Gdk::ACTION_MOVE | Gdk::ACTION_COPY) ); + } + + preview->signal_drag_data_get().connect(sigc::mem_fun(*this, &ColorItem::_dragGetColorData)); + preview->signal_drag_begin().connect(sigc::mem_fun(*this, &ColorItem::drag_begin)); + preview->signal_enter_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleEnterNotify)); + preview->signal_leave_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleLeaveNotify)); + + widget = preview; + } + + _previews.push_back( widget ); + + return widget; +} + +void ColorItem::buttonClicked(bool secondary) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + char const * attrName = secondary ? "stroke" : "fill"; + + SPCSSAttr *css = sp_repr_css_attr_new(); + Glib::ustring descr; + switch (def.getType()) { + case ege::PaintDef::CLEAR: { + // TODO actually make this clear + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Remove stroke color") : _("Remove fill color"); + break; + } + case ege::PaintDef::NONE: { + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Set stroke color to none") : _("Set fill color to none"); + break; + } +//mark + case ege::PaintDef::RGB: { + Glib::ustring colorspec; + if ( _grad ){ + colorspec = "url(#"; + colorspec += _grad->getId(); + colorspec += ")"; + } else { + gchar c[64]; + guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff; + sp_svg_write_color(c, sizeof(c), rgba); + colorspec = c; + } +//end mark + sp_repr_css_set_property( css, attrName, colorspec.c_str() ); + descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch"); + break; + } + } + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + + DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_SWATCHES, descr.c_str() ); + } +} + +void ColorItem::_wireMagicColors( SwatchPage *colorSet ) +{ + if ( colorSet ) + { + for ( boost::ptr_vector::iterator it = colorSet->_colors.begin(); it != colorSet->_colors.end(); ++it ) + { + std::string::size_type pos = it->def.descr.find("*{"); + if ( pos != std::string::npos ) + { + std::string subby = it->def.descr.substr( pos + 2 ); + std::string::size_type endPos = subby.find("}*"); + if ( endPos != std::string::npos ) + { + subby.erase( endPos ); + //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str()); + //g_message(" '%s'", subby.c_str()); + + if ( subby.find('E') != std::string::npos ) + { + it->def.setEditable( true ); + } + + if ( subby.find('L') != std::string::npos ) + { + it->_isLive = true; + } + + std::string part; + // Tint. index + 1 more val. + if ( getBlock( part, 'T', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + it->_linkTint( colorSet->_colors[colorIndex], percent ); + } + } + } + + // Shade/tone. index + 1 or 2 more val. + if ( getBlock( part, 'S', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + guint64 grayLevel = 0; + if ( !popVal( grayLevel, part ) ) { + grayLevel = 0; + } + it->_linkTone( colorSet->_colors[colorIndex], percent, grayLevel ); + } + } + } + + } + } + } + } +} + + +void ColorItem::_linkTint( ColorItem& other, int percent ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = false; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = 0; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = true; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = grayLevel; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +} // 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 : diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h new file mode 100644 index 0000000..7210ed2 --- /dev/null +++ b/src/ui/dialog/color-item.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_COLOR_ITEM_H +#define SEEN_DIALOGS_COLOR_ITEM_H + +#include + +#include "widgets/ege-paint-def.h" +#include "ui/previewable.h" + +class SPGradient; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ColorItem; + +class SwatchPage +{ +public: + SwatchPage(); + ~SwatchPage(); + + Glib::ustring _name; + int _prefWidth; + boost::ptr_vector _colors; +}; + + +/** + * The color swatch you see on screen as a clickable box. + */ +class ColorItem : public Inkscape::UI::Previewable +{ + friend void _loadPaletteFile( gchar const *filename ); +public: + ColorItem( ege::PaintDef::ColorType type ); + ColorItem( unsigned int r, unsigned int g, unsigned int b, + Glib::ustring& name ); + ~ColorItem() override; + ColorItem(ColorItem const &other); + virtual ColorItem &operator=(ColorItem const &other); + Gtk::Widget* getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) override; + void buttonClicked(bool secondary = false); + + void setGradient(SPGradient *grad); + SPGradient * getGradient() const { return _grad; } + void setPattern(cairo_pattern_t *pattern); + void setName(const Glib::ustring name); + + void setState( bool fill, bool stroke ); + bool isFill() { return _isFill; } + bool isStroke() { return _isStroke; } + + ege::PaintDef def; + +private: + + static void _dropDataIn( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data); + + void _dragGetColorData(const Glib::RefPtr &drag_context, + Gtk::SelectionData &data, + guint info, + guint time); + + static void _wireMagicColors( SwatchPage *colorSet ); + static void _colorDefChanged(void* data); + + void _updatePreviews(); + void _regenPreview(UI::Widget::Preview * preview); + + void _linkTint( ColorItem& other, int percent ); + void _linkTone( ColorItem& other, int percent, int grayLevel ); + void drag_begin(const Glib::RefPtr &dc); + void handleClick(); + void handleSecondaryClick(gint arg1); + bool handleEnterNotify(GdkEventCrossing* event); + bool handleLeaveNotify(GdkEventCrossing* event); + + std::vector _previews; + + bool _isFill; + bool _isStroke; + bool _isLive; + bool _linkIsTone; + int _linkPercent; + int _linkGray; + ColorItem* _linkSrc; + SPGradient* _grad; + cairo_pattern_t *_pattern; + std::vector _listeners; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_COLOR_ITEM_H + +/* + 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 : diff --git a/src/ui/dialog/debug.cpp b/src/ui/dialog/debug.cpp new file mode 100644 index 0000000..af2f08b --- /dev/null +++ b/src/ui/dialog/debug.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dialog that displays log messages. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include + +#include "debug.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A very simple dialog for displaying Inkscape messages - implementation. + */ +class DebugDialogImpl : public DebugDialog, public Gtk::Dialog +{ +public: + DebugDialogImpl(); + ~DebugDialogImpl() override; + + void show() override; + void hide() override; + void clear() override; + void message(char const *msg) override; + void captureLogMessages() override; + void releaseLogMessages() override; + +private: + Gtk::MenuBar menuBar; + Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; +}; + +void DebugDialogImpl::clear() +{ + Glib::RefPtr buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + +DebugDialogImpl::DebugDialogImpl() +{ + set_title(_("Messages")); + set_size_request(300, 400); + auto mainVBox = get_content_area(); + + //## Add a menu for clear() + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem(_("_File"), true)); + item->set_submenu(fileMenu); + menuBar.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("_Clear"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::clear)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Capture log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::captureLogMessages)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Release log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::releaseLogMessages)); + fileMenu.append(*item); + + mainVBox->pack_start(menuBar, Gtk::PACK_SHRINK); + + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + mainVBox->pack_start(textScroll); + + show_all_children(); + + message("ready."); + message("enable log display by setting "); + message("dialogs.debug 'redirect' attribute to 1 in preferences.xml"); + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; +} + + +DebugDialog *DebugDialog::create() +{ + DebugDialog *dialog = new DebugDialogImpl(); + return dialog; +} + +DebugDialogImpl::~DebugDialogImpl() += default; + +void DebugDialogImpl::show() +{ + //call super() + Gtk::Dialog::show(); + //sp_transientize(GTK_WIDGET(gobj())); //Make transient + raise(); + Gtk::Dialog::present(); +} + +void DebugDialogImpl::hide() +{ + // call super + Gtk::Dialog::hide(); +} + +void DebugDialogImpl::message(char const *msg) +{ + Glib::RefPtr buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +/* static instance, to reduce dependencies */ +static DebugDialog *debugDialogInstance = nullptr; + +DebugDialog *DebugDialog::getInstance() +{ + if (!debugDialogInstance) { + debugDialogInstance = new DebugDialogImpl(); + } + return debugDialogInstance; +} + + + +void DebugDialog::showInstance() +{ + DebugDialog *debugDialog = getInstance(); + debugDialog->show(); + // this is not a real memleak because getInstance() only creates a debug dialog once, and returns that instance for all subsequent calls + // cppcheck-suppress memleak +} + + + + +/*##### THIS IS THE IMPORTANT PART ##### */ +static void dialogLoggingFunction(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + DebugDialogImpl *dlg = static_cast(user_data); + dlg->message(messageText); +} + + +void DebugDialogImpl::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + message("log capture started"); +} + +void DebugDialogImpl::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message("log capture discontinued"); +} + + + +} //namespace Dialogs +} //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 : diff --git a/src/ui/dialog/debug.h b/src/ui/dialog/debug.h new file mode 100644 index 0000000..4520c73 --- /dev/null +++ b/src/ui/dialog/debug.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for displaying Inkscape messages + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOGS_DEBUGDIALOG_H +#define SEEN_UI_DIALOGS_DEBUGDIALOG_H + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * @brief A very simple dialog for displaying Inkscape messages. + * + * Messages sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +class DebugDialog +{ +public: + DebugDialog() = default;; + /** + * Factory method + */ + static DebugDialog *create(); + + /** + * Destructor + */ + virtual ~DebugDialog() = default;; + + + /** + * Show the dialog + */ + virtual void show() = 0; + + /** + * Do not show the dialog + */ + virtual void hide() = 0; + + /** + * @brief Clear all information from the dialog + * + * Also a public method. Remove all text from the dialog + */ + virtual void clear() = 0; + + /** + * Display a message + */ + virtual void message(char const *msg) = 0; + + /** + * Redirect g_log() messages to this widget + */ + virtual void captureLogMessages() = 0; + + /** + * Return g_log() messages to normal handling + */ + virtual void releaseLogMessages() = 0; + + /** + * Factory method. Use this to create a new DebugDialog + */ + static DebugDialog *getInstance(); + + /** + * Show the instance above + */ + static void showInstance(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif /* __DEBUGDIALOG_H__ */ + +/* + 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 : diff --git a/src/ui/dialog/desktop-tracker.cpp b/src/ui/dialog/desktop-tracker.cpp new file mode 100644 index 0000000..67f2cef --- /dev/null +++ b/src/ui/dialog/desktop-tracker.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widgets/desktop-widget.h" + +#include "desktop-tracker.h" + +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +DesktopTracker::DesktopTracker() : + base(nullptr), + desktop(nullptr), + widget(nullptr), + hierID(0), + trackActive(false), + desktopChangedSig() +{ +} + +DesktopTracker::~DesktopTracker() +{ + disconnect(); +} + +void DesktopTracker::connect(GtkWidget *widget) +{ + disconnect(); + + this->widget = widget; + + // Use C/gobject callbacks to avoid gtkmm rewrap-during-destruct issues: + hierID = g_signal_connect( G_OBJECT(widget), "hierarchy-changed", G_CALLBACK(hierarchyChangeCB), this ); + inkID = INKSCAPE.signal_activate_desktop.connect( + sigc::bind( + sigc::ptr_fun(&DesktopTracker::activateDesktopCB), this) + ); + + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } +} + +void DesktopTracker::disconnect() +{ + if (hierID) { + if (widget) { + g_signal_handler_disconnect(G_OBJECT(widget), hierID); + } + hierID = 0; + } + if (inkID.connected()) { + inkID.disconnect(); + } +} + +void DesktopTracker::setBase(SPDesktop *desktop) +{ + if (this->base != desktop) { + base = desktop; + // Do not override an existing target desktop + if (!this->desktop) { + setDesktop(desktop); + } + } +} + +SPDesktop *DesktopTracker::getBase() const +{ + return base; +} + +SPDesktop *DesktopTracker::getDesktop() const +{ + return desktop; +} + +sigc::connection DesktopTracker::connectDesktopChanged( const sigc::slot & slot ) +{ + return desktopChangedSig.connect(slot); +} + +void DesktopTracker::activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ) +{ + if (self && self->trackActive) { + self->setDesktop(desktop); + } + //return FALSE; +} + +bool DesktopTracker::hierarchyChangeCB(GtkWidget * /*widget*/, GtkWidget* /*prev*/, DesktopTracker *self) +{ + if (self) { + self->handleHierarchyChange(); + } + return false; +} + +void DesktopTracker::handleHierarchyChange() +{ + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + bool newFlag = (wdgt == nullptr); // true means not in an SPDesktopWidget, thus floating. + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } + if (newFlag != trackActive) { + trackActive = newFlag; + if (trackActive) { + setDesktop(SP_ACTIVE_DESKTOP); + } else if (desktop != base) { + setDesktop(getBase()); + } + } +} + +void DesktopTracker::setDesktop(SPDesktop *desktop) +{ + if (desktop != this->desktop) { + this->desktop = desktop; + desktopChangedSig.emit(desktop); + } +} + + +} // 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 : diff --git a/src/ui/dialog/desktop-tracker.h b/src/ui/dialog/desktop-tracker.h new file mode 100644 index 0000000..7b94390 --- /dev/null +++ b/src/ui/dialog/desktop-tracker.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOG_DESKTOP_TRACKER +#define SEEN_DIALOG_DESKTOP_TRACKER + +#include +#include + +typedef struct _GtkWidget GtkWidget; +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { + +namespace UI { +namespace Dialog { + +class DesktopTracker +{ +public: + DesktopTracker(); + virtual ~DesktopTracker(); + + void connect(GtkWidget *widget); + void disconnect(); + + SPDesktop *getDesktop() const; + + void setBase(SPDesktop *desktop); + SPDesktop *getBase() const; + + sigc::connection connectDesktopChanged( const sigc::slot & slot ); + +private: + static void activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ); + static bool hierarchyChangeCB(GtkWidget *widget, GtkWidget* prev, DesktopTracker *self); + + void handleHierarchyChange(); + void setDesktop(SPDesktop *desktop); + + SPDesktop *base; + SPDesktop *desktop; + GtkWidget *widget; + unsigned long hierID; + sigc::connection inkID; + bool trackActive; + sigc::signal desktopChangedSig; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOG_DESKTOP_TRACKER +/* + 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 : diff --git a/src/ui/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp new file mode 100644 index 0000000..02bb270 --- /dev/null +++ b/src/ui/dialog/dialog-manager.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Authors: + * Bryce W. Harrington + * Jon Phillips + * Gustav Broberg + * + * Copyright (C) 2004-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/dialog-manager.h" + +#include "style.h" +#include "ui/dialog/align-and-distribute.h" +#include "ui/dialog/document-metadata.h" +#include "ui/dialog/document-properties.h" +#include "ui/dialog/extension-editor.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/dialog/filter-editor.h" +#include "ui/dialog/filter-effects-dialog.h" +#include "ui/dialog/find.h" +#include "ui/dialog/glyphs.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/dialog/input.h" +#include "ui/dialog/livepatheffect-editor.h" +#include "ui/dialog/memory.h" +#include "ui/dialog/messages.h" +#include "ui/dialog/paint-servers.h" +#include "ui/dialog/symbols.h" +#include "ui/dialog/tile.h" +# include "ui/dialog/tracedialog.h" + +#include "ui/dialog/transformation.h" +#include "ui/dialog/undo-history.h" +#include "ui/dialog/panel-dialog.h" +#include "ui/dialog/layers.h" +#include "ui/dialog/icon-preview.h" +//#include "ui/dialog/print-colors-preview-dialog.h" +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/export.h" +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/object-properties.h" +#include "ui/dialog/objects.h" +#include "ui/dialog/selectorsdialog.h" + +#if HAVE_ASPELL +# include "ui/dialog/spellcheck.h" +#endif + +#include "ui/dialog/tags.h" + +#include "ui/dialog/styledialog.h" +#include "ui/dialog/svg-fonts-dialog.h" +#include "ui/dialog/text-edit.h" +#include "ui/dialog/xml-tree.h" +#include "util/ege-appear-time-tracker.h" +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +using namespace Behavior; + +template +inline Dialog *create() { return PanelDialog::template create(); } + +} + +/** + * This class is provided as a container for Inkscape's various + * dialogs. This allows InkscapeApplication to treat the various + * dialogs it invokes, as abstractions. + * + * DialogManager is essentially a cache of dialogs. It lets us + * initialize dialogs lazily - instead of constructing them during + * application startup, they're constructed the first time they're + * actually invoked by InkscapeApplication. The constructed + * dialog is held here after that, so future invocations of the + * dialog don't need to get re-constructed each time. The memory for + * the dialogs are then reclaimed when the DialogManager is destroyed. + * + * In addition, DialogManager also serves as a signal manager for + * dialogs. It provides a set of signals that can be sent to all + * dialogs for doing things such as hiding/unhiding them, etc. + * DialogManager ensures that every dialog it handles will listen + * to these signals. + * + */ +DialogManager::DialogManager() { + + using namespace Behavior; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + // The preferences dialog is broken, the DockBehavior code resizes it's floating window to the smallest size + registerFactory("InkscapePreferences", &create); + + if (dialogs_type == FLOATING) { + registerFactory("AlignAndDistribute", &create); + registerFactory("DocumentMetadata", &create); + registerFactory("DocumentProperties", &create); + registerFactory("ExtensionEditor", &create); + registerFactory("FillAndStroke", &create); + registerFactory("FilterEffectsDialog", &create); + registerFactory("FilterEditorDialog", &create); + registerFactory("Find", &create); + registerFactory("Glyphs", &create); + registerFactory("IconPreviewPanel", &create); + registerFactory("LayersPanel", &create); + registerFactory("ObjectsPanel", &create); + registerFactory("TagsPanel", &create); + registerFactory("LivePathEffect", &create); + registerFactory("Memory", &create); + registerFactory("Messages", &create); + registerFactory("ObjectAttributes", &create); + registerFactory("ObjectProperties", &create); +// registerFactory("PrintColorsPreviewDialog", &create); + registerFactory("SvgFontsDialog", &create); + registerFactory("Swatches", &create); + registerFactory("TileDialog", &create); + registerFactory("Symbols", &create); + registerFactory("PaintServers", &create); + registerFactory("StyleDialog", &create); + registerFactory("Trace", &create); + + registerFactory("Transformation", &create); + registerFactory("UndoHistory", &create); + registerFactory("InputDevices", &create); + registerFactory("TextFont", &create); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create); +#endif + + registerFactory("Export", &create); + registerFactory("CloneTiler", &create); + registerFactory("XmlTree", &create); + registerFactory("Selectors", &create); + + } else { + + registerFactory("AlignAndDistribute", &create); + registerFactory("DocumentMetadata", &create); + registerFactory("DocumentProperties", &create); + registerFactory("ExtensionEditor", &create); + registerFactory("FillAndStroke", &create); + registerFactory("FilterEffectsDialog", &create); + registerFactory("FilterEditorDialog", &create); + registerFactory("Find", &create); + registerFactory("Glyphs", &create); + registerFactory("IconPreviewPanel", &create); + registerFactory("LayersPanel", &create); + registerFactory("ObjectsPanel", &create); + registerFactory("TagsPanel", &create); + registerFactory("LivePathEffect", &create); + registerFactory("Memory", &create); + registerFactory("Messages", &create); + registerFactory("ObjectAttributes", &create); + registerFactory("ObjectProperties", &create); +// registerFactory("PrintColorsPreviewDialog", &create); + registerFactory("SvgFontsDialog", &create); + registerFactory("Swatches", &create); + registerFactory("TileDialog", &create); + registerFactory("Symbols", &create); + registerFactory("PaintServers", &create); + registerFactory("Trace", &create); + + registerFactory("Transformation", &create); + registerFactory("UndoHistory", &create); + registerFactory("InputDevices", &create); + registerFactory("TextFont", &create); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create); +#endif + + registerFactory("Export", &create); + registerFactory("CloneTiler", &create); + registerFactory("XmlTree", &create); + registerFactory("Selectors", &create); + } +} + +DialogManager::~DialogManager() { + // TODO: Disconnect the signals + // TODO: Do we need to explicitly delete the dialogs? + // Appears to cause a segfault if we do +} + + +DialogManager &DialogManager::getInstance() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + /* Use singleton behavior for floating dialogs */ + if (dialogs_type == FLOATING) { + static DialogManager *instance = nullptr; + + if (!instance) + instance = new DialogManager(); + return *instance; + } + + return *new DialogManager(); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(gchar const *name, + DialogManager::DialogFactory factory) +{ + registerFactory(g_quark_from_string(name), factory); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(GQuark name, + DialogManager::DialogFactory factory) +{ + _factory_map[name] = factory; +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(gchar const *name) { + return getDialog(g_quark_from_string(name)); +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(GQuark name) { + DialogMap::iterator dialog_found; + dialog_found = _dialog_map.find(name); + + Dialog *dialog=nullptr; + if ( dialog_found != _dialog_map.end() ) { + dialog = dialog_found->second; + } else { + FactoryMap::iterator factory_found; + factory_found = _factory_map.find(name); + + if ( factory_found != _factory_map.end() ) { + dialog = factory_found->second(); + _dialog_map[name] = dialog; + } + } + + return dialog; +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(gchar const *name, bool grabfocus) { + showDialog(g_quark_from_string(name), grabfocus); +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(GQuark name, bool /*grabfocus*/) { + bool wantTiming = Inkscape::Preferences::get()->getBool("/dialogs/debug/trackAppear", false); + GTimer *timer = (wantTiming) ? g_timer_new() : nullptr; // if needed, must be created/started before getDialog() + Dialog *dialog = getDialog(name); + if ( dialog ) { + if ( wantTiming ) { + gchar const * nameStr = g_quark_to_string(name); + ege::AppearTimeTracker *tracker = new ege::AppearTimeTracker(timer, dialog->gobj(), nameStr); + tracker->setAutodelete(true); + timer = nullptr; + } + // should check for grabfocus, but lp:1348927 prevents it + dialog->present(); + } + + if ( timer ) { + g_timer_destroy(timer); + timer = 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 : diff --git a/src/ui/dialog/dialog-manager.h b/src/ui/dialog/dialog-manager.h new file mode 100644 index 0000000..5b22189 --- /dev/null +++ b/src/ui/dialog/dialog-manager.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Author: + * Bryce W. Harrington + * Jon Phillips + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MANAGER_H +#define INKSCAPE_UI_DIALOG_MANAGER_H + +#include "dialog.h" +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogManager { +public: + typedef Dialog *(*DialogFactory)(); + + DialogManager(); + virtual ~DialogManager(); + + static DialogManager &getInstance(); + + // sigc::signal show_dialogs; + // sigc::signal show_f12; + // sigc::signal hide_dialogs; + // sigc::signal hide_f12; + // sigc::signal transientize; + + /* generic dialog management start */ + typedef std::map FactoryMap; + typedef std::map DialogMap; + + void registerFactory(gchar const *name, DialogFactory factory); + void registerFactory(GQuark name, DialogFactory factory); + Dialog *getDialog(gchar const* dlgName); + Dialog *getDialog(GQuark dlgName); + void showDialog(gchar const *name, bool grabfocus=true); + void showDialog(GQuark name, bool grabfocus=true); + +protected: + DialogManager(DialogManager const &d); // no copy + DialogManager& operator=(DialogManager const &d); // no assign + + FactoryMap _factory_map; //< factories to create dialogs + DialogMap _dialog_map; //< map of already created dialogs +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_MANAGER_H + +/* + 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 : diff --git a/src/ui/dialog/dialog.cpp b/src/ui/dialog/dialog.cpp new file mode 100644 index 0000000..cf94097 --- /dev/null +++ b/src/ui/dialog/dialog.cpp @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Base class for dialogs in Inkscape - implementation. + */ +/* Authors: + * Bryce W. Harrington + * buliabyak@gmail.com + * Johan Engelen + * Gustav Broberg + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-manager.h" +#include + +#include + +#include + +#include "inkscape.h" +#include "ui/monitor.h" +#include "ui/tools/tool-base.h" +#include "desktop.h" + +#include "shortcuts.h" +#include "ui/interface.h" +#include "verbs.h" +#include "ui/tool/event-utils.h" + +#define MIN_ONSCREEN_DISTANCE 50 + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +gboolean sp_retransientize_again(gpointer dlgPtr) +{ + Dialog *dlg = static_cast(dlgPtr); + dlg->retransientize_suppress = false; + return FALSE; // so that it is only called once +} + +//===================================================================== + +Dialog::Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path, int verb_num, + Glib::ustring apply_label) + : _user_hidden(false), + _hiddenF12(false), + retransientize_suppress(false), + _prefs_path(prefs_path), + _verb_num(verb_num), + _title(), + _apply_label(std::move(apply_label)), + _desktop(nullptr), + _is_active_desktop(true), + _behavior(nullptr) +{ + gchar title[500]; + + if (verb_num) { + sp_ui_dialog_title_string (Inkscape::Verb::get(verb_num), title); + } + + _title = title; + _behavior = behavior_factory(*this); + _desktop = SP_ACTIVE_DESKTOP; + + Gtk::Widget *widg = dynamic_cast(Glib::wrap(_behavior->gobj())); + INKSCAPE.signal_activate_desktop.connect(sigc::mem_fun(*this, &Dialog::onDesktopActivated)); + INKSCAPE.signal_dialogs_hide.connect(sigc::mem_fun(*this, &Dialog::onHideF12)); + INKSCAPE.signal_dialogs_unhide.connect(sigc::mem_fun(*this, &Dialog::onShowF12)); + INKSCAPE.signal_shut_down.connect(sigc::mem_fun(*this, &Dialog::onShutdown)); + INKSCAPE.signal_change_theme.connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes), widg)); + + Glib::wrap(gobj())->signal_event().connect(sigc::mem_fun(*this, &Dialog::_onEvent)); + Glib::wrap(gobj())->signal_key_press_event().connect(sigc::mem_fun(*this, &Dialog::_onKeyPress)); + + read_geometry(); + sp_add_top_window_classes(widg); +} + +Dialog::~Dialog() +{ + save_geometry(); + delete _behavior; + _behavior = nullptr; +} + + +//--------------------------------------------------------------------- + + +void Dialog::onDesktopActivated(SPDesktop *desktop) +{ + _is_active_desktop = (desktop == _desktop); + _behavior->onDesktopActivated(desktop); +} + +void Dialog::onShutdown() +{ + save_geometry(); + //_user_hidden = true; + _behavior->onShutdown(); +} + +void Dialog::onHideF12() +{ + _hiddenF12 = true; + _behavior->onHideF12(); +} + +void Dialog::onShowF12() +{ + if (_user_hidden) + return; + + if (_hiddenF12) { + _behavior->onShowF12(); + } + + _hiddenF12 = false; +} + + +inline Dialog::operator Gtk::Widget &() { return *_behavior; } +inline GtkWidget *Dialog::gobj() { return _behavior->gobj(); } +inline void Dialog::present() { _behavior->present(); } +inline Gtk::Box *Dialog::get_vbox() { return _behavior->get_vbox(); } +inline void Dialog::hide() { _behavior->hide(); } +inline void Dialog::show() { _behavior->show(); } +inline void Dialog::show_all_children() { _behavior->show_all_children(); } +inline void Dialog::set_size_request(int width, int height) { _behavior->set_size_request(width, height); } +inline void Dialog::size_request(Gtk::Requisition &requisition) { _behavior->size_request(requisition); } +inline void Dialog::get_position(int &x, int &y) { _behavior->get_position(x, y); } +inline void Dialog::get_size(int &width, int &height) { _behavior->get_size(width, height); } +inline void Dialog::resize(int width, int height) { _behavior->resize(width, height); } +inline void Dialog::move(int x, int y) { _behavior->move(x, y); } +inline void Dialog::set_position(Gtk::WindowPosition position) { _behavior->set_position(position); } +inline void Dialog::set_title(Glib::ustring title) { _behavior->set_title(title); } +inline void Dialog::set_sensitive(bool sensitive) { _behavior->set_sensitive(sensitive); } + +Glib::SignalProxy0 Dialog::signal_show() { return _behavior->signal_show(); } +Glib::SignalProxy0 Dialog::signal_hide() { return _behavior->signal_hide(); } + +void Dialog::read_geometry() +{ + _user_hidden = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int x = prefs->getInt(_prefs_path + "/x", -1000); + int y = prefs->getInt(_prefs_path + "/y", -1000); + int w = prefs->getInt(_prefs_path + "/w", 0); + int h = prefs->getInt(_prefs_path + "/h", 0); + + // g_print ("read %d %d %d %d\n", x, y, w, h); + + // If there are stored height and width values for the dialog, + // resize the window to match; otherwise we leave it at its default + if (w != 0 && h != 0) { + resize(w, h); + } + + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + auto const screen_width = monitor_geometry.get_width(); + auto const screen_height = monitor_geometry.get_height(); + + // If there are stored values for where the dialog should be + // located, then restore the dialog to that position. + // also check if (x,y) is actually onscreen with the current screen dimensions + if ( (x >= 0) && (y >= 0) && (x < (screen_width-MIN_ONSCREEN_DISTANCE)) && (y < (screen_height-MIN_ONSCREEN_DISTANCE)) ) { + move(x, y); + } else { + // ...otherwise just put it in the middle of the screen + set_position(Gtk::WIN_POS_CENTER); + } + +} + + +void Dialog::save_geometry() +{ + int y, x, w, h; + + get_position(x, y); + get_size(w, h); + + // g_print ("write %d %d %d %d\n", x, y, w, h); + + if (x<0) x=0; + if (y<0) y=0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(_prefs_path + "/x", x); + prefs->setInt(_prefs_path + "/y", y); + prefs->setInt(_prefs_path + "/w", w); + prefs->setInt(_prefs_path + "/h", h); + +} + +void +Dialog::save_status(int visible, int state, int placement) +{ + // Only save dialog status for dialogs on the "last document" + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop != nullptr || !_is_active_desktop ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs) { + prefs->setInt(_prefs_path + "/visible", visible); + prefs->setInt(_prefs_path + "/state", state); + prefs->setInt(_prefs_path + "/placement", placement); + } +} + + +void Dialog::_handleResponse(int response_id) +{ + switch (response_id) { + case Gtk::RESPONSE_CLOSE: { + _close(); + break; + } + } +} + +bool Dialog::_onDeleteEvent(GdkEventAny */*event*/) +{ + save_geometry(); + _user_hidden = true; + + return false; +} + +bool Dialog::_onEvent(GdkEvent *event) +{ + bool ret = false; + + switch (event->type) { + case GDK_KEY_PRESS: { + switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: { + _defocus(); + ret = true; + break; + } + case GDK_KEY_F4: + case GDK_KEY_w: + case GDK_KEY_W: { + if (Inkscape::UI::held_only_control(event->key)) { + _close(); + ret = true; + } + break; + } + default: { // pass keypress to the canvas + break; + } + } + } + default: + ; + } + + return ret; +} + +bool Dialog::_onKeyPress(GdkEventKey *event) +{ + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event((GdkEventKey*)event); + return sp_shortcut_invoke(shortcut, SP_ACTIVE_DESKTOP); +} + +void Dialog::_apply() +{ + g_warning("Apply button clicked for dialog [Dialog::_apply()]"); +} + +void Dialog::_close() +{ + _behavior->hide(); + _onDeleteEvent(nullptr); +} + +void Dialog::_defocus() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(desktop->canvas)); + + // make sure the canvas window is present before giving it focus + Gtk::Window *toplevel_window = dynamic_cast(canvas->get_toplevel()); + if (toplevel_window) + toplevel_window->present(); + + canvas->grab_focus(); + } +} + +Inkscape::Selection* +Dialog::_getSelection() +{ + return SP_ACTIVE_DESKTOP->getSelection(); +} + +void sp_add_top_window_classes_callback(Gtk::Widget *widg) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(desktop->canvas)); + Gtk::Window *toplevel_window = dynamic_cast(canvas->get_toplevel()); + if (toplevel_window) { + Gtk::Window *current_window = dynamic_cast(widg); + if (!current_window) { + current_window = dynamic_cast(widg->get_toplevel()); + } + if (current_window) { + if (toplevel_window->get_style_context()->has_class("dark")) { + current_window->get_style_context()->add_class("dark"); + current_window->get_style_context()->remove_class("bright"); + } else { + current_window->get_style_context()->add_class("bright"); + current_window->get_style_context()->remove_class("dark"); + } + if (toplevel_window->get_style_context()->has_class("symbolic")) { + current_window->get_style_context()->add_class("symbolic"); + current_window->get_style_context()->remove_class("regular"); + } else { + current_window->get_style_context()->remove_class("symbolic"); + current_window->get_style_context()->add_class("regular"); + } + } + } + } +} + +void sp_add_top_window_classes(Gtk::Widget *widg) +{ + if (!widg) { + return; + } + if (!widg->get_realized()) { + widg->signal_realize().connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes_callback), widg)); + } else { + sp_add_top_window_classes_callback(widg); + } +} + +} // 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 : diff --git a/src/ui/dialog/dialog.h b/src/ui/dialog/dialog.h new file mode 100644 index 0000000..012bbc6 --- /dev/null +++ b/src/ui/dialog/dialog.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Base class for dialogs in Inkscape + */ +/* Authors: + * Bryce W. Harrington + * Gustav Broberg + * Kris De Gussem + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_H +#define INKSCAPE_DIALOG_H + +#include "dock-behavior.h" +#include "floating-behavior.h" + +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { +class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +enum BehaviorType { FLOATING, DOCK }; + +gboolean sp_retransientize_again(gpointer dlgPtr); +void sp_dialog_shutdown(GObject *object, gpointer dlgPtr); + +/** + * Base class for Inkscape dialogs. + * + * UI::Dialog::Dialog is a base class for all dialogs in Inkscape. The + * purpose of this class is to provide a unified place for ensuring + * style and behavior. Specifically, this class provides functionality + * for saving and restoring the size and position of dialogs (through + * the user's preferences file). + * + * It also provides some general purpose signal handlers for things like + * showing and hiding all dialogs. + * + * Fundamental parts of the dialog's behavior are controlled by + * a UI::Dialog::Behavior subclass instance connected to the dialog. + * + * @see UI::Widget::Panel panel class from which the dialogs are actually derived from. + * @see UI::Dialog::DialogManager manages the dialogs within inkscape. + * @see UI::Dialog::PanelDialog which links Panel and Dialog together in a dockable and floatable dialog. + */ +class Dialog { + +public: + + /** + * Constructor. + * + * @param behavior_factory floating or docked. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path = nullptr, + int verb_num = 0, Glib::ustring apply_label = ""); + + virtual ~Dialog(); + + virtual void onDesktopActivated(SPDesktop*); + virtual void onShutdown(); + + /* Hide and show dialogs */ + virtual void onHideF12(); + virtual void onShowF12(); + + virtual operator Gtk::Widget &(); + virtual GtkWidget *gobj(); + virtual void present(); + virtual Gtk::Box *get_vbox(); + virtual void show(); + virtual void hide(); + virtual void show_all_children(); + virtual void set_size_request(int, int); + virtual void size_request(Gtk::Requisition &); + virtual void get_position(int &x, int &y); + virtual void get_size(int &width, int &height); + virtual void resize(int width, int height); + virtual void move(int x, int y); + virtual void set_position(Gtk::WindowPosition position); + virtual void set_title(Glib::ustring title); + virtual void set_sensitive(bool sensitive=true); + + virtual Glib::SignalProxy0 signal_show(); + virtual Glib::SignalProxy0 signal_hide(); + + bool _user_hidden; // when it is closed by the user, to prevent repopping on f12 + bool _hiddenF12; + + /** + * Read window position from preferences. + */ + void read_geometry(); + + /** + * Save window position to preferences. + */ + void save_geometry(); + void save_status(int visible, int state, int placement); + + bool retransientize_suppress; // when true, do not retransientize (prevents races when switching new windows too fast) + +protected: + Glib::ustring const _prefs_path; + int _verb_num; + Glib::ustring _title; + Glib::ustring _apply_label; + SPDesktop * _desktop; + bool _is_active_desktop; + + virtual void _handleResponse(int response_id); + + virtual bool _onDeleteEvent (GdkEventAny*); + virtual bool _onEvent(GdkEvent *event); + virtual bool _onKeyPress(GdkEventKey *event); + + virtual void _apply(); + + /* Closes the dialog window. + * + * This code sends a delete_event to the dialog, + * instead of just destroying it, so that the + * dialog can do some housekeeping, such as remember + * its position. + */ + virtual void _close(); + virtual void _defocus(); + + Inkscape::Selection* _getSelection(); + + sigc::connection _desktop_activated_connection; + sigc::connection _dialogs_hidden_connection; + sigc::connection _dialogs_unhidden_connection; + sigc::connection _shutdown_connection; + sigc::connection _change_theme_connection; + + private: + Behavior::Behavior* _behavior; + + Dialog() = delete; // no constructor without params + + Dialog(Dialog const &d) = delete; // no copy + Dialog& operator=(Dialog const &d) = delete; // no assign + + friend class Behavior::FloatingBehavior; + friend class Behavior::DockBehavior; +}; + +void sp_add_top_window_classes(Gtk::Widget *widg); +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + + +#endif //INKSCAPE_DIALOG_H + +/* + 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 : diff --git a/src/ui/dialog/dock-behavior.cpp b/src/ui/dialog/dock-behavior.cpp new file mode 100644 index 0000000..9ea9d07 --- /dev/null +++ b/src/ui/dialog/dock-behavior.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" +#include "ui/widget/dock.h" +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + + +DockBehavior::DockBehavior(Dialog &dialog) : + Behavior(dialog), + _dock_item(*SP_ACTIVE_DESKTOP->getDock(), + Inkscape::Verb::get(dialog._verb_num)->get_id(), dialog._title.c_str(), + (Inkscape::Verb::get(dialog._verb_num)->get_image() ? + Inkscape::Verb::get(dialog._verb_num)->get_image() : ""), + static_cast( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/state", + UI::Widget::DockItem::DOCKED_STATE)), + static_cast( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/placement", + GDL_DOCK_TOP))) + +{ + // Connect signals + _signal_hide_connection = signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onHide)); + signal_show().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onShow)); + _dock_item.signal_state_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onStateChanged)); + if (_dock_item.getState() == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) { + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } + } +} + +DockBehavior::~DockBehavior() += default; + + +Behavior * +DockBehavior::create(Dialog &dialog) +{ + return new DockBehavior(dialog); +} + + +DockBehavior::operator Gtk::Widget &() +{ + return _dock_item.getWidget(); +} + +GtkWidget * +DockBehavior::gobj() +{ + return _dock_item.gobj(); +} + +Gtk::VBox * +DockBehavior::get_vbox() +{ + return _dock_item.get_vbox(); +} + +void +DockBehavior::present() +{ + bool was_attached = _dock_item.isAttached(); + + _dock_item.present(); + + if (!was_attached) + _dialog.read_geometry(); +} + +void +DockBehavior::hide() +{ + _signal_hide_connection.block(); + _dock_item.hide(); + _signal_hide_connection.unblock(); +} + +void +DockBehavior::show() +{ + _dock_item.show(); +} + +void +DockBehavior::show_all_children() +{ + get_vbox()->show_all_children(); +} + +void +DockBehavior::get_position(int &x, int &y) +{ + _dock_item.get_position(x, y); +} + +void +DockBehavior::get_size(int &width, int &height) +{ + _dock_item.get_size(width, height); +} + +void +DockBehavior::resize(int width, int height) +{ + _dock_item.resize(width, height); +} + +void +DockBehavior::move(int x, int y) +{ + _dock_item.move(x, y); +} + +void +DockBehavior::set_position(Gtk::WindowPosition position) +{ + _dock_item.set_position(position); +} + +void +DockBehavior::set_size_request(int width, int height) +{ + _dock_item.set_size_request(width, height); +} + +void +DockBehavior::size_request(Gtk::Requisition &requisition) +{ + _dock_item.size_request(requisition); +} + +void +DockBehavior::set_title(Glib::ustring title) +{ + _dock_item.set_title(title); +} + +void DockBehavior::set_sensitive(bool sensitive) +{ + // TODO check this. Seems to be bad that we ignore the parameter + get_vbox()->set_sensitive(); +} + + +void +DockBehavior::_onHide() +{ + _dialog.save_geometry(); + _dialog._user_hidden = true; +} + +void +DockBehavior::_onShow() +{ + _dialog._user_hidden = false; +} + +void +DockBehavior::_onStateChanged(Widget::DockItem::State /*prev_state*/, + Widget::DockItem::State new_state) +{ +// TODO probably need to avoid window calls unless the state is different. Check. + + if (new_state == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } +} + +void +DockBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +DockBehavior::onShowF12() +{ + present(); +} + +void +DockBehavior::onShutdown() +{ + int visible = _dock_item.isIconified() || !_dialog._user_hidden; + int status = (_dock_item.getState() == Inkscape::UI::Widget::DockItem::UNATTACHED) ? _dock_item.getPrevState() : _dock_item.getState(); + _dialog.save_status( visible, status, _dock_item.getPlacement() ); +} + +void +DockBehavior::onDesktopActivated(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited( "/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + Gtk::Window *floating_win = _dock_item.getWindow(); + + if (floating_win) { + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (GtkWindow *dialog_win = floating_win->gobj()) { + + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); + } +} + + +/* Signal wrappers */ + +Glib::SignalProxy0 +DockBehavior::signal_show() { return _dock_item.signal_show(); } + +Glib::SignalProxy0 +DockBehavior::signal_hide() { return _dock_item.signal_hide(); } + +Glib::SignalProxy1 +DockBehavior::signal_delete_event() { return _dock_item.signal_delete_event(); } + +Glib::SignalProxy0 +DockBehavior::signal_drag_begin() { return _dock_item.signal_drag_begin(); } + +Glib::SignalProxy1 +DockBehavior::signal_drag_end() { return _dock_item.signal_drag_end(); } + + +} // namespace Behavior +} // 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 : diff --git a/src/ui/dialog/dock-behavior.h b/src/ui/dialog/dock-behavior.h new file mode 100644 index 0000000..85b8244 --- /dev/null +++ b/src/ui/dialog/dock-behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H + +#include "ui/widget/dock-item.h" +#include "behavior.h" + +namespace Gtk { + class Paned; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class DockBehavior : public Behavior { + +public: + static Behavior *create(Dialog& dialog); + + ~DockBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget&() override; + GtkWidget *gobj() override; + void present() override; + Gtk::VBox *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition& requisition) override; + void get_position(int& x, int& y) override; + void get_size(int& width, int& height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0 signal_show() override; + Glib::SignalProxy0 signal_hide() override; + Glib::SignalProxy1 signal_delete_event() override; + Glib::SignalProxy0 signal_drag_begin(); + Glib::SignalProxy1 signal_drag_end(); + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + Widget::DockItem _dock_item; + + DockBehavior(Dialog& dialog); + + /** Internal helpers */ + Gtk::Paned *_getPaned(); //< gives the parent pane, if the dock item has one + void _requestHeight(int height); //< tries to resize the dock item to the requested height + + /** Internal signal handlers */ + void _onHide(); + void _onShow(); + bool _onDeleteEvent(GdkEventAny *event); + void _onStateChanged(Widget::DockItem::State prev_state, Widget::DockItem::State new_state); + bool _onKeyPress(GdkEventKey *event); + + sigc::connection _signal_hide_connection; + sigc::connection _signal_key_press_event_connection; + +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H + +/* + 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 : diff --git a/src/ui/dialog/document-metadata.cpp b/src/ui/dialog/document-metadata.cpp new file mode 100644 index 0000000..a4c2707 --- /dev/null +++ b/src/ui/dialog/document-metadata.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document metadata dialog, Gtkmm-style. + */ +/* Authors: + * bulia byak + * Bryce W. Harrington + * Lauris Kaplinski + * Jon Phillips + * Ralf Stephan (Gtkmm) + * + * Copyright (C) 2000 - 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document-metadata.h" +#include "desktop.h" +#include "rdf.h" +#include "verbs.h" + +#include "object/sp-namedview.h" + +#include "ui/widget/entity-entry.h" +#include "xml/node-event-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 15 + +//=================================================== + +//--------------------------------------------------- + +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +DocumentMetadata & +DocumentMetadata::getInstance() +{ + DocumentMetadata &instance = *new DocumentMetadata(); + instance.init(); + return instance; +} + + +DocumentMetadata::DocumentMetadata() + : UI::Widget::Panel("/dialogs/documentmetadata", SP_VERB_DIALOG_METADATA) +{ + hide(); + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _page_metadata1.set_border_width(4); + _page_metadata2.set_border_width(4); + + _page_metadata1.set_column_spacing(2); + _page_metadata2.set_column_spacing(2); + _page_metadata1.set_row_spacing(2); + _page_metadata2.set_row_spacing(2); + + _notebook.append_page(_page_metadata1, _("Metadata")); + _notebook.append_page(_page_metadata2, _("License")); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDeactivateDesktop)); + + build_metadata(); +} + +void +DocumentMetadata::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + + show_all_children(); +} + +DocumentMetadata::~DocumentMetadata() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + + for (auto & it : _rdflist) + delete it; +} + +// TODO: This duplicates code in document-properties.cpp +void +DocumentMetadata::build_metadata() +{ + using Inkscape::UI::Widget::EntityEntry; + + _page_metadata1.show(); + + Gtk::Label *label = Gtk::manage (new Gtk::Label); + label->set_markup (_("Dublin Core Entities")); + label->set_halign(Gtk::ALIGN_START); + label->set_valign(Gtk::ALIGN_CENTER); + + _page_metadata1.attach(*label, 0, 0, 2, 1); + + /* add generic metadata entry areas */ + struct rdf_work_entity_t * entity; + int row = 1; + for (entity = rdf_work_entities; entity && entity->name; entity++, row++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + EntityEntry *w = EntityEntry::create (entity, _wr); + _rdflist.push_back (w); + + w->_label.set_halign(Gtk::ALIGN_START); + w->_label.set_valign(Gtk::ALIGN_CENTER); + _page_metadata1.attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1.attach(*w->_packable, 1, row, 1, 1); + } + } + + _page_metadata2.show(); + + row = 0; + Gtk::Label *llabel = Gtk::manage (new Gtk::Label); + llabel->set_markup (_("License")); + llabel->set_halign(Gtk::ALIGN_START); + llabel->set_valign(Gtk::ALIGN_CENTER); + _page_metadata2.attach(*llabel, 0, row, 2, 1); + + /* add license selector pull-down and URI */ + ++row; + _licensor.init (_wr); + + _licensor.set_hexpand(); + _licensor.set_valign(Gtk::ALIGN_CENTER); + _page_metadata2.attach(_licensor, 1, row, 1, 1); +} + +/** + * Update dialog widgets from desktop. + */ +void DocumentMetadata::update() +{ + if (_wr.isUpdating()) return; + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + _wr.setUpdating (false); +} + +void +DocumentMetadata::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); +} + +//-------------------------------------------------------------------- + +/** + * Called when XML node attribute changed; updates dialog widgets. + */ +static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data) +{ + if (DocumentMetadata *dialog = static_cast(data)) + dialog->update(); +} + +} // 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 : diff --git a/src/ui/dialog/document-metadata.h b/src/ui/dialog/document-metadata.h new file mode 100644 index 0000000..7004dca --- /dev/null +++ b/src/ui/dialog/document-metadata.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Metadata dialog + */ +/* Authors: + * Ralf Stephan + * Bryce W. Harrington + * + * Copyright (C) 2004, 2005, 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H + +#include +#include +#include "ui/widget/panel.h" +#include +#include + +#include "inkscape.h" +#include "ui/widget/licensor.h" +#include "ui/widget/registry.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + } + namespace Dialog { + +typedef std::list RDElist; + +class DocumentMetadata : public Inkscape::UI::Widget::Panel { +public: + void update(); + + static DocumentMetadata &getInstance(); + + static void destroy(); + +protected: + void build_metadata(); + void init(); + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + Gtk::Notebook _notebook; + + Gtk::Grid _page_metadata1; + Gtk::Grid _page_metadata2; + + //--------------------------------------------------------------- + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + UI::Widget::Registry _wr; + +private: + ~DocumentMetadata() override; + DocumentMetadata(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H + +/* + 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 : diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp new file mode 100644 index 0000000..31f59ec --- /dev/null +++ b/src/ui/dialog/document-properties.cpp @@ -0,0 +1,1708 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document properties dialog, Gtkmm-style. + */ +/* Authors: + * bulia byak + * Bryce W. Harrington + * Lauris Kaplinski + * Jon Phillips + * Ralf Stephan (Gtkmm) + * Diederik van Lierop + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Johan Engelen + * Copyright (C) 2000 - 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include +#include "style.h" +#include "rdf.h" +#include "verbs.h" + +#include "display/canvas-grid.h" +#include "document-properties.h" +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "io/sys.h" +#include "object/sp-root.h" +#include "object/sp-script.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/widget/entity-entry.h" +#include "ui/widget/notebook-page.h" +#include "xml/node-event-vector.h" + +#if defined(HAVE_LIBLCMS2) +#include "object/color-profile.h" +#endif // defined(HAVE_LIBLCMS2) + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 10 + + +//=================================================== + +//--------------------------------------------------- + +static void on_child_added(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + on_child_added, // child_added + on_child_removed, // child_removed + on_repr_attr_changed, + nullptr, // content_changed + nullptr // order_changed +}; + +static void docprops_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + +DocumentProperties& DocumentProperties::getInstance() +{ + DocumentProperties &instance = *new DocumentProperties(); + instance.init(); + + return instance; +} + +DocumentProperties::DocumentProperties() + : UI::Widget::Panel("/dialogs/documentoptions", SP_VERB_DIALOG_NAMEDVIEW) + , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_guides(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_snap(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_cms(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_scripting(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_external_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_embedded_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata1(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata2(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + //--------------------------------------------------------------- + , _rcb_antialias(_("Use antialiasing"), _("If unset, no antialiasing will be done on the drawing"), "shape-rendering", _wr, false, nullptr, nullptr, nullptr, "crispEdges") + , _rcb_checkerboard(_("Checkerboard background"), _("If set, use a colored checkerboard for the canvas background"), "inkscape:pagecheckerboard", _wr, false) + , _rcb_canb(_("Show page _border"), _("If set, rectangular page border is shown"), "showborder", _wr, false) + , _rcb_bord(_("Border on _top of drawing"), _("If set, border is always on top of the drawing"), "borderlayer", _wr, false) + , _rcb_shad(_("_Show border shadow"), _("If set, page border shows a shadow on its right and lower side"), "inkscape:showpageshadow", _wr, false) + , _rcp_bg(_("Back_ground color:"), _("Background color"), _("Color of the canvas background. Note: opacity is ignored except when exporting to bitmap."), "pagecolor", "inkscape:pageopacity", _wr) + , _rcp_bord(_("Border _color:"), _("Page border color"), _("Color of the page border"), "bordercolor", "borderopacity", _wr) + , _rum_deflt(_("Display _units:"), "inkscape:document-units", _wr) + , _page_sizer(_wr) + //--------------------------------------------------------------- + //General snap options + , _rcb_sgui(_("Show _guides"), _("Show or hide guides"), "showguides", _wr) + , _rcb_lgui(_("Lock all guides"), _("Toggle lock of all guides in the document"), "inkscape:lockguides", _wr) + , _rcp_gui(_("Guide co_lor:"), _("Guideline color"), _("Color of guidelines"), "guidecolor", "guideopacity", _wr) + , _rcp_hgui(_("_Highlight color:"), _("Highlighted guideline color"), _("Color of a guideline when it is under mouse"), "guidehicolor", "guidehiopacity", _wr) + , _create_guides_btn(_("Create guides around the page")) + , _delete_guides_btn(_("Delete all guides")) + //--------------------------------------------------------------- + , _rsu_sno(_("Snap _distance"), _("Snap only when _closer than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to objects"), _("Always snap to objects, regardless of their distance"), + _("If set, objects only snap to another object when it's within the range specified below"), + "objecttolerance", _wr) + //Options for snapping to grids + , _rsu_sn(_("Snap d_istance"), _("Snap only when c_loser than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to grid"), _("Always snap to grids, regardless of the distance"), + _("If set, objects only snap to a grid line when it's within the range specified below"), + "gridtolerance", _wr) + //Options for snapping to guides + , _rsu_gusn(_("Snap dist_ance"), _("Snap only when close_r than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to guides"), _("Always snap to guides, regardless of the distance"), + _("If set, objects only snap to a guide when it's within the range specified below"), + "guidetolerance", _wr) + //--------------------------------------------------------------- + , _rcb_snclp(_("Snap to clip paths"), _("When snapping to paths, then also try snapping to clip paths"), "inkscape:snap-path-clip", _wr) + , _rcb_snmsk(_("Snap to mask paths"), _("When snapping to paths, then also try snapping to mask paths"), "inkscape:snap-path-mask", _wr) + , _rcb_perp(_("Snap perpendicularly"), _("When snapping to paths or guides, then also try snapping perpendicularly"), "inkscape:snap-perpendicular", _wr) + , _rcb_tang(_("Snap tangentially"), _("When snapping to paths or guides, then also try snapping tangentially"), "inkscape:snap-tangential", _wr) + //--------------------------------------------------------------- + , _grids_label_crea("", Gtk::ALIGN_START) + , _grids_button_new(C_("Grid", "_New"), _("Create new grid.")) + , _grids_button_remove(C_("Grid", "_Remove"), _("Remove selected grid.")) + , _grids_label_def("", Gtk::ALIGN_START) +{ + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _notebook.append_page(*_page_page, _("Page")); + _notebook.append_page(*_page_guides, _("Guides")); + _notebook.append_page(_grids_vbox, _("Grids")); + _notebook.append_page(*_page_snap, _("Snap")); + _notebook.append_page(*_page_cms, _("Color")); + _notebook.append_page(*_page_scripting, _("Scripting")); + _notebook.append_page(*_page_metadata1, _("Metadata")); + _notebook.append_page(*_page_metadata2, _("License")); + + _wr.setUpdating (true); + build_page(); + build_guides(); + build_gridspage(); + build_snap(); +#if defined(HAVE_LIBLCMS2) + build_cms(); +#endif // defined(HAVE_LIBLCMS2) + build_scripting(); + build_metadata(); + _wr.setUpdating (false); + + _grids_button_new.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onNewGrid)); + _grids_button_remove.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onRemoveGrid)); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDeactivateDesktop)); + + _rum_deflt._changed_connection.block(); + _rum_deflt.getUnitMenu()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::onDocUnitChange)); +} + +void DocumentProperties::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->addListener (&_repr_events, this); + + show_all_children(); + _grids_button_remove.hide(); +} + +DocumentProperties::~DocumentProperties() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->removeListenerByData (this); + + for (auto & it : _rdflist) + delete it; +} + +//======================================================================== + +/** + * Helper function that sets widgets in a 2 by n table. + * arr has two entries per table row. Each row is in the following form: + * widget, widget -> function adds a widget in each column. + * nullptr, widget -> function adds a widget that occupies the row. + * label, nullptr -> function adds label that occupies the row. + * nullptr, nullptr -> function adds an empty box that occupies the row. + * This used to be a helper function for a 3 by n table + */ +void attach_all(Gtk::Grid &table, Gtk::Widget *const arr[], unsigned const n) +{ + for (unsigned i = 0, r = 0; i < n; i += 2) { + if (arr[i] && arr[i+1]) { + arr[i]->set_hexpand(); + arr[i+1]->set_hexpand(); + arr[i]->set_valign(Gtk::ALIGN_CENTER); + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + table.attach(*arr[i], 0, r, 1, 1); + table.attach(*arr[i+1], 1, r, 1, 1); + } else { + if (arr[i+1]) { + Gtk::AttachOptions yoptions = (Gtk::AttachOptions)0; + if (dynamic_cast(arr[i+1])) { + // only the PageSizer in Document Properties|Page should be stretched vertically + yoptions = Gtk::FILL|Gtk::EXPAND; + } + arr[i+1]->set_hexpand(); + + if (yoptions & Gtk::EXPAND) + arr[i+1]->set_vexpand(); + else + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + + table.attach(*arr[i+1], 0, r, 2, 1); + } else if (arr[i]) { + Gtk::Label& label = reinterpret_cast(*arr[i]); + + label.set_hexpand(); + label.set_halign(Gtk::ALIGN_START); + label.set_valign(Gtk::ALIGN_CENTER); + table.attach(label, 0, r, 2, 1); + } else { + auto space = Gtk::manage (new Gtk::Box); + space->set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + space->set_halign(Gtk::ALIGN_CENTER); + space->set_valign(Gtk::ALIGN_CENTER); + table.attach(*space, 0, r, 1, 1); + } + } + ++r; + } +} + +void DocumentProperties::build_page() +{ + _page_page->show(); + + Gtk::Label* label_gen = Gtk::manage (new Gtk::Label); + label_gen->set_markup (_("General")); + + Gtk::Label *label_for = Gtk::manage (new Gtk::Label); + label_for->set_markup (_("Page Size")); + + Gtk::Label* label_bkg = Gtk::manage (new Gtk::Label); + label_bkg->set_markup (_("Background")); + + Gtk::Label* label_bdr = Gtk::manage (new Gtk::Label); + label_bdr->set_markup (_("Border")); + + Gtk::Label* label_dsp = Gtk::manage (new Gtk::Label); + label_dsp->set_markup (_("Display")); + + _page_sizer.init(); + + _rcb_doc_props_left.set_border_width(4); + _rcb_doc_props_left.set_row_spacing(4); + _rcb_doc_props_left.set_column_spacing(4); + _rcb_doc_props_right.set_border_width(4); + _rcb_doc_props_right.set_row_spacing(4); + _rcb_doc_props_right.set_column_spacing(4); + + Gtk::Widget *const widget_array[] = + { + label_gen, nullptr, + nullptr, &_rum_deflt, + nullptr, nullptr, + label_for, nullptr, + nullptr, &_page_sizer, + nullptr, nullptr, + &_rcb_doc_props_left, &_rcb_doc_props_right, + }; + attach_all(_page_page->table(), widget_array, G_N_ELEMENTS(widget_array)); + + Gtk::Widget *const widget_array_left[] = + { + label_bkg, nullptr, + nullptr, &_rcb_checkerboard, + nullptr, &_rcp_bg, + label_dsp, nullptr, + nullptr, &_rcb_antialias, + }; + attach_all(_rcb_doc_props_left, widget_array_left, G_N_ELEMENTS(widget_array_left)); + + Gtk::Widget *const widget_array_right[] = + { + label_bdr, nullptr, + nullptr, &_rcb_canb, + nullptr, &_rcb_bord, + nullptr, &_rcb_shad, + nullptr, &_rcp_bord, + }; + attach_all(_rcb_doc_props_right, widget_array_right, G_N_ELEMENTS(widget_array_right)); + + std::list _slaveList; + _slaveList.push_back(&_rcb_bord); + _slaveList.push_back(&_rcb_shad); + _slaveList.push_back(&_rcp_bord); + _rcb_canb.setSlaveWidgets(_slaveList); +} + +void DocumentProperties::build_guides() +{ + _page_guides->show(); + + Gtk::Label *label_gui = Gtk::manage (new Gtk::Label); + label_gui->set_markup (_("Guides")); + + _rum_deflt.set_margin_start(0); + _rcp_bg.set_margin_start(0); + _rcp_bord.set_margin_start(0); + _rcp_gui.set_margin_start(0); + _rcp_hgui.set_margin_start(0); + _rcp_gui.set_hexpand(); + _rcp_hgui.set_hexpand(); + _rcb_sgui.set_hexpand(); + auto inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + inner->add(_rcb_sgui); + inner->add(_rcb_lgui); + inner->add(_rcp_gui); + inner->add(_rcp_hgui); + auto spacer = Gtk::manage(new Gtk::Label()); + Gtk::Widget *const widget_array[] = + { + label_gui, nullptr, + inner, spacer, + nullptr, nullptr, + nullptr, &_create_guides_btn, + nullptr, &_delete_guides_btn + }; + attach_all(_page_guides->table(), widget_array, G_N_ELEMENTS(widget_array)); + inner->set_hexpand(false); + + _create_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::create_guides_around_page)); + _delete_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::delete_all_guides)); +} + +void DocumentProperties::build_snap() +{ + _page_snap->show(); + + Gtk::Label *label_o = Gtk::manage (new Gtk::Label); + label_o->set_markup (_("Snap to objects")); + Gtk::Label *label_gr = Gtk::manage (new Gtk::Label); + label_gr->set_markup (_("Snap to grids")); + Gtk::Label *label_gu = Gtk::manage (new Gtk::Label); + label_gu->set_markup (_("Snap to guides")); + Gtk::Label *label_m = Gtk::manage (new Gtk::Label); + label_m->set_markup (_("Miscellaneous")); + + auto spacer = Gtk::manage(new Gtk::Label()); + + Gtk::Widget *const array[] = + { + label_o, nullptr, + nullptr, _rsu_sno._vbox, + &_rcb_snclp, spacer, + nullptr, &_rcb_snmsk, + nullptr, nullptr, + label_gr, nullptr, + nullptr, _rsu_sn._vbox, + nullptr, nullptr, + label_gu, nullptr, + nullptr, _rsu_gusn._vbox, + nullptr, nullptr, + label_m, nullptr, + nullptr, &_rcb_perp, + nullptr, &_rcb_tang + }; + attach_all(_page_snap->table(), array, G_N_ELEMENTS(array)); + } + +void DocumentProperties::create_guides_around_page() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_GUIDES_AROUND_PAGE ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void DocumentProperties::delete_all_guides() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_DELETE_ALL_GUIDES ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +#if defined(HAVE_LIBLCMS2) +/// Populates the available color profiles combo box +void DocumentProperties::populate_available_profiles(){ + _AvailableProfilesListStore->clear(); // Clear any existing items in the combo box + + // Iterate through the list of profiles and add the name to the combo box. + bool home = true; // initial value doesn't matter, it's just to avoid a compiler warning + bool first = true; + for (auto &profile: ColorProfile::getProfileFilesWithNames()) { + Gtk::TreeModel::Row row; + + // add a separator between profiles from the user's home directory and system profiles + if (!first && profile.isInHome != home) + { + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = ""; + row[_AvailableProfilesListColumns.nameColumn] = ""; + row[_AvailableProfilesListColumns.separatorColumn] = true; + } + home = profile.isInHome; + first = false; + + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = profile.filename; + row[_AvailableProfilesListColumns.nameColumn] = profile.name; + row[_AvailableProfilesListColumns.separatorColumn] = false; + } +} + +/** + * Cleans up name to remove disallowed characters. + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + * + * @param str the string to clean up. + */ +static void sanitizeName( Glib::ustring& str ) +{ + if (str.size() > 0) { + char val = str.at(0); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && (val != '_') + && (val != ':')) { + str.insert(0, "_"); + } + for (Glib::ustring::size_type i = 1; i < str.size(); i++) { + char val = str.at(i); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && ((val < '0') || (val > '9')) + && (val != '_') + && (val != ':') + && (val != '-') + && (val != '.')) { + str.replace(i, 1, "-"); + } + } + } +} + +/// Links the selected color profile in the combo box to the document +void DocumentProperties::linkSelectedProfile() +{ + //store this profile in the SVG document (create element in the XML) + // TODO remove use of 'active' desktop + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + // Find the index of the currently-selected row in the color profiles combobox + Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active(); + + if (!iter) { + g_warning("No color profile available."); + return; + } + + // Read the filename and description from the list of available profiles + Glib::ustring file = (*iter)[_AvailableProfilesListColumns.fileColumn]; + Glib::ustring name = (*iter)[_AvailableProfilesListColumns.nameColumn]; + + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast(obj); + if (!strcmp(prof->href, file.c_str())) + return; + } + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile"); + gchar* tmp = g_strdup(name.c_str()); + Glib::ustring nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions + sanitizeName(nameStr); + cprofRepr->setAttribute("name", nameStr); + cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file))); + cprofRepr->setAttribute("id", file); + + + // Checks whether there is a defs element. Creates it when needed + Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs"); + if (!defsRepr) { + defsRepr = xml_doc->createElement("svg:defs"); + xml_doc->root()->addChild(defsRepr, nullptr); + } + + g_assert(desktop->doc()->getDefs()); + defsRepr->addChild(cprofRepr, nullptr); + + // TODO check if this next line was sometimes needed. It being there caused an assertion. + //Inkscape::GC::release(defsRepr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_LINK_COLOR_PROFILE, _("Link Color Profile")); + + populate_linked_profiles_box(); + } +} + +struct _cmp { + bool operator()(const SPObject * const & a, const SPObject * const & b) + { + const Inkscape::ColorProfile &a_prof = reinterpret_cast(*a); + const Inkscape::ColorProfile &b_prof = reinterpret_cast(*b); + gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; + } +}; + +template +struct static_caster { To * operator () (From * value) const { return static_cast(value); } }; + +void DocumentProperties::populate_linked_profiles_box() +{ + _LinkedProfilesListStore->clear(); + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + if (! current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + + std::set _current; + std::transform(current.begin(), + current.end(), + std::inserter(_current, _current.begin()), + static_caster()); + + for (auto &profile: _current) { + Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append()); + row[_LinkedProfilesListColumns.nameColumn] = profile->name; +// row[_LinkedProfilesListColumns.previewColumn] = "Color Preview"; + } +} + +void DocumentProperties::external_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _ExternalScriptsContextMenu.popup_at_pointer(reinterpret_cast(event)); + } +} + +void DocumentProperties::embedded_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbeddedScriptsContextMenu.popup_at_pointer(reinterpret_cast(event)); + } +} + +void DocumentProperties::linked_profiles_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbProfContextMenu.popup_at_pointer(reinterpret_cast(event)); + } +} + +void DocumentProperties::cms_create_popup_menu(Gtk::Widget& parent, sigc::slot rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbProfContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbProfContextMenu.accelerate(parent); +} + + +void DocumentProperties::external_create_popup_menu(Gtk::Widget& parent, sigc::slot rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _ExternalScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _ExternalScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbeddedScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbeddedScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::onColorProfileSelectRow() +{ + Glib::RefPtr sel = _LinkedProfilesList.get_selection(); + if (sel) { + _unlink_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + + +void DocumentProperties::removeSelectedProfile(){ + Glib::ustring name; + if(_LinkedProfilesList.get_selection()) { + Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_LinkedProfilesListColumns.nameColumn]; + } else { + return; + } + } + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast(obj); + if (!name.compare(prof->name)){ + prof->deleteObject(true, false); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_COLOR_PROFILE, _("Remove linked color profile")); + break; // removing the color profile likely invalidates part of the traversed list, stop traversing here. + } + } + + populate_linked_profiles_box(); + onColorProfileSelectRow(); +} + +bool DocumentProperties::_AvailableProfilesList_separator(const Glib::RefPtr& model, const Gtk::TreeModel::iterator& iter) +{ + bool separator = (*iter)[_AvailableProfilesListColumns.separatorColumn]; + return separator; +} + +void DocumentProperties::build_cms() +{ + _page_cms->show(); + Gtk::Label *label_link= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_link->set_markup (_("Linked Color Profiles:")); + Gtk::Label *label_avail = Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_avail->set_markup (_("Available Color Profiles:")); + + _link_btn.set_tooltip_text(_("Link Profile")); + docprops_style_button(_link_btn, INKSCAPE_ICON("list-add")); + + _unlink_btn.set_tooltip_text(_("Unlink Profile")); + docprops_style_button(_unlink_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_link->set_hexpand(); + label_link->set_halign(Gtk::ALIGN_START); + label_link->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_link, 0, row, 3, 1); + + row++; + + _LinkedProfilesListScroller.set_hexpand(); + _LinkedProfilesListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_LinkedProfilesListScroller, 0, row, 3, 1); + + row++; + + Gtk::HBox* spacer = Gtk::manage(new Gtk::HBox()); + spacer->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer->set_hexpand(); + spacer->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*spacer, 0, row, 3, 1); + + row++; + + label_avail->set_hexpand(); + label_avail->set_halign(Gtk::ALIGN_START); + label_avail->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_avail, 0, row, 3, 1); + + row++; + + _AvailableProfilesList.set_hexpand(); + _AvailableProfilesList.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_AvailableProfilesList, 0, row, 1, 1); + + _link_btn.set_halign(Gtk::ALIGN_CENTER); + _link_btn.set_valign(Gtk::ALIGN_CENTER); + _link_btn.set_margin_start(2); + _link_btn.set_margin_end(2); + _page_cms->table().attach(_link_btn, 1, row, 1, 1); + + _unlink_btn.set_halign(Gtk::ALIGN_CENTER); + _unlink_btn.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_unlink_btn, 2, row, 1, 1); + + // Set up the Available Profiles combo box + _AvailableProfilesListStore = Gtk::ListStore::create(_AvailableProfilesListColumns); + _AvailableProfilesList.set_model(_AvailableProfilesListStore); + _AvailableProfilesList.pack_start(_AvailableProfilesListColumns.nameColumn); + _AvailableProfilesList.set_row_separator_func(sigc::mem_fun(*this, &DocumentProperties::_AvailableProfilesList_separator)); + + populate_available_profiles(); + + //# Set up the Linked Profiles combo box + _LinkedProfilesListStore = Gtk::ListStore::create(_LinkedProfilesListColumns); + _LinkedProfilesList.set_model(_LinkedProfilesListStore); + _LinkedProfilesList.append_column(_("Profile Name"), _LinkedProfilesListColumns.nameColumn); +// _LinkedProfilesList.append_column(_("Color Preview"), _LinkedProfilesListColumns.previewColumn); + _LinkedProfilesList.set_headers_visible(false); +// TODO restore? _LinkedProfilesList.set_fixed_height_mode(true); + + populate_linked_profiles_box(); + + _LinkedProfilesListScroller.add(_LinkedProfilesList); + _LinkedProfilesListScroller.set_shadow_type(Gtk::SHADOW_IN); + _LinkedProfilesListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _LinkedProfilesListScroller.set_size_request(-1, 90); + + _link_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile)); + _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); + + _LinkedProfilesList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::linked_profiles_list_button_release)); + cms_create_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "defs" ); + if (!current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); + onColorProfileSelectRow(); +} +#endif // defined(HAVE_LIBLCMS2) + +void DocumentProperties::build_scripting() +{ + _page_scripting->show(); + + _page_scripting->table().attach(_scripting_notebook, 0, 0, 1, 1); + + _scripting_notebook.append_page(*_page_external_scripts, _("External scripts")); + _scripting_notebook.append_page(*_page_embedded_scripts, _("Embedded scripts")); + + //# External scripts tab + _page_external_scripts->show(); + Gtk::Label *label_external= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_external->set_markup (_("External script files:")); + + _external_add_btn.set_tooltip_text(_("Add the current file name or browse for a file")); + docprops_style_button(_external_add_btn, INKSCAPE_ICON("list-add")); + + _external_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_external_remove_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_external->set_hexpand(); + label_external->set_halign(Gtk::ALIGN_START); + label_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*label_external, 0, row, 3, 1); + + row++; + + _ExternalScriptsListScroller.set_hexpand(); + _ExternalScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_ExternalScriptsListScroller, 0, row, 3, 1); + + row++; + + Gtk::HBox* spacer_external = Gtk::manage(new Gtk::HBox()); + spacer_external->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer_external->set_hexpand(); + spacer_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*spacer_external, 0, row, 3, 1); + + row++; + + _script_entry.set_hexpand(); + _script_entry.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_script_entry, 0, row, 1, 1); + + _external_add_btn.set_halign(Gtk::ALIGN_CENTER); + _external_add_btn.set_valign(Gtk::ALIGN_CENTER); + _external_add_btn.set_margin_start(2); + _external_add_btn.set_margin_end(2); + + _page_external_scripts->table().attach(_external_add_btn, 1, row, 1, 1); + + _external_remove_btn.set_halign(Gtk::ALIGN_CENTER); + _external_remove_btn.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_external_remove_btn, 2, row, 1, 1); + + //# Set up the External Scripts box + _ExternalScriptsListStore = Gtk::ListStore::create(_ExternalScriptsListColumns); + _ExternalScriptsList.set_model(_ExternalScriptsListStore); + _ExternalScriptsList.append_column(_("Filename"), _ExternalScriptsListColumns.filenameColumn); + _ExternalScriptsList.set_headers_visible(true); +// TODO restore? _ExternalScriptsList.set_fixed_height_mode(true); + + + //# Embedded scripts tab + _page_embedded_scripts->show(); + Gtk::Label *label_embedded= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded->set_markup (_("Embedded script files:")); + + _embed_new_btn.set_tooltip_text(_("New")); + docprops_style_button(_embed_new_btn, INKSCAPE_ICON("list-add")); + + _embed_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_embed_remove_btn, INKSCAPE_ICON("list-remove")); + + _embed_button_box.set_layout (Gtk::BUTTONBOX_START); + _embed_button_box.add(_embed_new_btn); + _embed_button_box.add(_embed_remove_btn); + + row = 0; + + label_embedded->set_hexpand(); + label_embedded->set_halign(Gtk::ALIGN_START); + label_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded, 0, row, 3, 1); + + row++; + + _EmbeddedScriptsListScroller.set_hexpand(); + _EmbeddedScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedScriptsListScroller, 0, row, 3, 1); + + row++; + + _embed_button_box.set_hexpand(); + _embed_button_box.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_embed_button_box, 0, row, 1, 1); + + row++; + + Gtk::HBox* spacer_embedded = Gtk::manage(new Gtk::HBox()); + spacer_embedded->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + spacer_embedded->set_hexpand(); + spacer_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*spacer_embedded, 0, row, 3, 1); + + row++; + + //# Set up the Embedded Scripts box + _EmbeddedScriptsListStore = Gtk::ListStore::create(_EmbeddedScriptsListColumns); + _EmbeddedScriptsList.set_model(_EmbeddedScriptsListStore); + _EmbeddedScriptsList.append_column(_("Script id"), _EmbeddedScriptsListColumns.idColumn); + _EmbeddedScriptsList.set_headers_visible(true); +// TODO restore? _EmbeddedScriptsList.set_fixed_height_mode(true); + + //# Set up the Embedded Scripts content box + Gtk::Label *label_embedded_content= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded_content->set_markup (_("Content:")); + + label_embedded_content->set_hexpand(); + label_embedded_content->set_halign(Gtk::ALIGN_START); + label_embedded_content->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded_content, 0, row, 3, 1); + + row++; + + _EmbeddedContentScroller.set_hexpand(); + _EmbeddedContentScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedContentScroller, 0, row, 3, 1); + + _EmbeddedContentScroller.add(_EmbeddedContent); + _EmbeddedContentScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedContentScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _EmbeddedContentScroller.set_size_request(-1, 140); + + _EmbeddedScriptsList.signal_cursor_changed().connect(sigc::mem_fun(*this, &DocumentProperties::changeEmbeddedScript)); + _EmbeddedScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onEmbeddedScriptSelectRow) ); + + _ExternalScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onExternalScriptSelectRow) ); + + _EmbeddedContent.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::editEmbeddedScript)); + + populate_script_lists(); + + _ExternalScriptsListScroller.add(_ExternalScriptsList); + _ExternalScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _ExternalScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _ExternalScriptsListScroller.set_size_request(-1, 90); + + _external_add_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addExternalScript)); + + _EmbeddedScriptsListScroller.add(_EmbeddedScriptsList); + _EmbeddedScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _EmbeddedScriptsListScroller.set_size_request(-1, 90); + + _embed_new_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addEmbeddedScript)); + +#if defined(HAVE_LIBLCMS2) + + _external_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + _embed_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); + + _ExternalScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::external_scripts_list_button_release)); + external_create_popup_menu(_ExternalScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + + _EmbeddedScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::embedded_scripts_list_button_release)); + embedded_create_popup_menu(_EmbeddedScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); +#endif // defined(HAVE_LIBLCMS2) + +//TODO: review this observers code: + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + if (! current.empty()) { + _scripts_observer.set((*(current.begin()))->parent); + } + _scripts_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_script_lists)); + onEmbeddedScriptSelectRow(); + onExternalScriptSelectRow(); +} + +// TODO: This duplicates code in document-metadata.cpp +void DocumentProperties::build_metadata() +{ + using Inkscape::UI::Widget::EntityEntry; + + _page_metadata1->show(); + + Gtk::Label *label = Gtk::manage (new Gtk::Label); + label->set_markup (_("Dublin Core Entities")); + label->set_halign(Gtk::ALIGN_START); + label->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach (*label, 0,0,2,1); + + /* add generic metadata entry areas */ + struct rdf_work_entity_t * entity; + int row = 1; + for (entity = rdf_work_entities; entity && entity->name; entity++, row++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + EntityEntry *w = EntityEntry::create (entity, _wr); + _rdflist.push_back (w); + + w->_label.set_halign(Gtk::ALIGN_START); + w->_label.set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(*w->_packable, 1, row, 1, 1); + } + } + + Gtk::Button *button_save = Gtk::manage (new Gtk::Button(_("_Save as default"),true)); + button_save->set_tooltip_text(_("Save this metadata as the default metadata")); + Gtk::Button *button_load = Gtk::manage (new Gtk::Button(_("Use _default"),true)); + button_load->set_tooltip_text(_("Use the previously saved default metadata here")); + + auto box_buttons = Gtk::manage (new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + box_buttons->pack_start(*button_save, true, true, 6); + box_buttons->pack_start(*button_load, true, true, 6); + _page_metadata1->pack_end(*box_buttons, false, false, 0); + + button_save->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::save_default_metadata)); + button_load->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::load_default_metadata)); + + _page_metadata2->show(); + + row = 0; + Gtk::Label *llabel = Gtk::manage (new Gtk::Label); + llabel->set_markup (_("License")); + llabel->set_halign(Gtk::ALIGN_START); + llabel->set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(*llabel, 0, row, 2, 1); + + /* add license selector pull-down and URI */ + ++row; + _licensor.init (_wr); + + _licensor.set_hexpand(); + _licensor.set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(_licensor, 0, row, 2, 1); +} + +void DocumentProperties::addExternalScript(){ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + g_warning("No active desktop"); + return; + } + + if (_script_entry.get_text().empty() ) { + // Click Add button with no filename, show a Browse dialog + browseExternalScript(); + } + + if (!_script_entry.get_text().empty()) { + + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + scriptRepr->setAttributeOrRemoveIfEmpty("xlink:href", _script_entry.get_text()); + _script_entry.set_text(""); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_ADD_EXTERNAL_SCRIPT, _("Add external script...")); + + populate_script_lists(); + } + +} + +static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr; + +void DocumentProperties::browseExternalScript() { + + //# Get the current directory for finding files + static Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + + Glib::ustring attr = prefs->getString(_prefs_path); + if (!attr.empty()) open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.empty()) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!selectPrefsFileInstance) { + selectPrefsFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::CUSTOM_TYPE, + _("Select a script to load")); + selectPrefsFileInstance->addFilterMenu("Javascript Files", "*.js"); + } + + //# Show the dialog + bool const success = selectPrefsFileInstance->show(); + + if (!success) { + return; + } + + //# User selected something. Get name and type + Glib::ustring fileName = selectPrefsFileInstance->getFilename(); + + _script_entry.set_text(fileName); +} + +void DocumentProperties::addEmbeddedScript(){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_ADD_EMBEDDED_SCRIPT, _("Add embedded script...")); + + populate_script_lists(); + } +} + +void DocumentProperties::removeExternalScript(){ + Glib::ustring name; + if(_ExternalScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _ExternalScriptsList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_ExternalScriptsListColumns.filenameColumn]; + } else { + return; + } + } + + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (obj) { + SPScript* script = dynamic_cast(obj); + if (script && (name == script->xlinkhref)) { + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EXTERNAL_SCRIPT, _("Remove external script")); + } + } + } + } + + populate_script_lists(); +} + +void DocumentProperties::removeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + SPObject* obj = SP_ACTIVE_DOCUMENT->getObjectById(id); + if (obj) { + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EMBEDDED_SCRIPT, _("Remove embedded script")); + } + } + + populate_script_lists(); +} + +void DocumentProperties::onExternalScriptSelectRow() +{ + Glib::RefPtr sel = _ExternalScriptsList.get_selection(); + if (sel) { + _external_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::onEmbeddedScriptSelectRow() +{ + Glib::RefPtr sel = _EmbeddedScriptsList.get_selection(); + if (sel) { + _embed_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::changeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + bool voidscript=true; + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (id == obj->getId()){ + int count = (int) obj->children.size(); + + if (count>1) + g_warning("TODO: Found a script element with multiple (%d) child nodes! We must implement support for that!", count); + + //XML Tree being used directly here while it shouldn't be. + SPObject* child = obj->firstChild(); + //TODO: shouldn't we get all children instead of simply the first child? + + if (child && child->getRepr()){ + const gchar* content = child->getRepr()->content(); + if (content){ + voidscript=false; + _EmbeddedContent.get_buffer()->set_text(content); + } + } + } + } + + if (voidscript) + _EmbeddedContent.get_buffer()->set_text(""); +} + +void DocumentProperties::editEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + Inkscape::XML::Document *xml_doc = SP_ACTIVE_DOCUMENT->getReprDoc(); + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (id == obj->getId()){ + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + auto tmp = obj->children | boost::adaptors::transformed([](SPObject& o) { return &o; }); + std::vector vec(tmp.begin(), tmp.end()); + for (auto &child: vec) { + child->deleteObject(); + } + obj->appendChildRepr(xml_doc->createTextNode(_EmbeddedContent.get_buffer()->get_text().c_str())); + + //TODO repr->set_content(_EmbeddedContent.get_buffer()->get_text()); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_EMBEDDED_SCRIPT, _("Edit embedded script")); + } + } + } +} + +void DocumentProperties::populate_script_lists(){ + _ExternalScriptsListStore->clear(); + _EmbeddedScriptsListStore->clear(); + std::vector current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + if (!current.empty()) { + SPObject *obj = *(current.begin()); + g_assert(obj != nullptr); + _scripts_observer.set(obj->parent); + } + for (auto obj : current) { + SPScript* script = dynamic_cast(obj); + g_assert(script != nullptr); + if (script->xlinkhref) + { + Gtk::TreeModel::Row row = *(_ExternalScriptsListStore->append()); + row[_ExternalScriptsListColumns.filenameColumn] = script->xlinkhref; + } + else // Embedded scripts + { + Gtk::TreeModel::Row row = *(_EmbeddedScriptsListStore->append()); + row[_EmbeddedScriptsListColumns.idColumn] = obj->getId(); + } + } +} + +/** +* Called for _updating_ the dialog (e.g. when a new grid was manually added in XML) +*/ +void DocumentProperties::update_gridspage() +{ + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + + int prev_page_count = _grids_notebook.get_n_pages(); + int prev_page_pos = _grids_notebook.get_current_page(); + + //remove all tabs + while (_grids_notebook.get_n_pages() != 0) { + _grids_notebook.remove_page(-1); // this also deletes the page. + } + + //add tabs + for(auto grid : nv->grids) { + if (!grid->repr->attribute("id")) continue; // update_gridspage is called again when "id" is added + Glib::ustring name(grid->repr->attribute("id")); + const char *icon = nullptr; + switch (grid->getGridType()) { + case GRID_RECTANGULAR: + icon = "grid-rectangular"; + break; + case GRID_AXONOMETRIC: + icon = "grid-axonometric"; + break; + default: + break; + } + _grids_notebook.append_page(*grid->newWidget(), _createPageTabLabel(name, icon)); + } + _grids_notebook.show_all(); + + int cur_page_count = _grids_notebook.get_n_pages(); + if (cur_page_count > 0) { + _grids_button_remove.set_sensitive(true); + + // The following is not correct if grid added/removed via XML + if (cur_page_count == prev_page_count + 1) { + _grids_notebook.set_current_page(cur_page_count - 1); + } else if (cur_page_count == prev_page_count) { + _grids_notebook.set_current_page(prev_page_pos); + } else if (cur_page_count == prev_page_count - 1) { + _grids_notebook.set_current_page(prev_page_pos < 1 ? 0 : prev_page_pos - 1); + } + } else { + _grids_button_remove.set_sensitive(false); + } +} + +/** + * Build grid page of dialog. + */ +void DocumentProperties::build_gridspage() +{ + /// \todo FIXME: gray out snapping when grid is off. + /// Dissenting view: you want snapping without grid. + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + (void)nv; + + _grids_label_crea.set_markup(_("Creation")); + _grids_label_def.set_markup(_("Defined grids")); + _grids_hbox_crea.pack_start(_grids_combo_gridtype, true, true); + _grids_hbox_crea.pack_start(_grids_button_new, true, true); + + for (gint t = 0; t <= GRID_MAXTYPENR; t++) { + _grids_combo_gridtype.append( CanvasGrid::getName( (GridType) t ) ); + } + _grids_combo_gridtype.set_active_text( CanvasGrid::getName(GRID_RECTANGULAR) ); + + _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + _grids_vbox.set_border_width(4); + _grids_vbox.set_spacing(4); + _grids_vbox.pack_start(_grids_label_crea, false, false); + _grids_vbox.pack_start(_grids_hbox_crea, false, false); + _grids_vbox.pack_start(_grids_space, false, false); + _grids_vbox.pack_start(_grids_label_def, false, false); + _grids_vbox.pack_start(_grids_notebook, false, false); + _grids_vbox.pack_start(_grids_button_remove, false, false); + + update_gridspage(); +} + + + +/** + * Update dialog widgets from desktop. Also call updateWidget routines of the grids. + */ +void DocumentProperties::update() +{ + if (_wr.isUpdating()) return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------page page + _rcb_checkerboard.setActive (nv->pagecheckerboard); + _rcp_bg.setRgba32 (nv->pagecolor); + _rcb_canb.setActive (nv->showborder); + _rcb_bord.setActive (nv->borderlayer == SP_BORDER_LAYER_TOP); + _rcp_bord.setRgba32 (nv->bordercolor); + _rcb_shad.setActive (nv->showpageshadow); + + SPRoot *root = dt->getDocument()->getRoot(); + _rcb_antialias.set_xml_target(root->getRepr(), dt->getDocument()); + _rcb_antialias.setActive(root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES); + + if (nv->display_units) { + _rum_deflt.setUnit (nv->display_units->abbr); + } + + double doc_w = dt->getDocument()->getRoot()->width.value; + Glib::ustring doc_w_unit = unit_table.getUnit(dt->getDocument()->getRoot()->width.unit)->abbr; + if (doc_w_unit == "") { + doc_w_unit = "px"; + } else if (doc_w_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_w_unit = "px"; + doc_w = dt->getDocument()->getRoot()->viewBox.width(); + } + double doc_h = dt->getDocument()->getRoot()->height.value; + Glib::ustring doc_h_unit = unit_table.getUnit(dt->getDocument()->getRoot()->height.unit)->abbr; + if (doc_h_unit == "") { + doc_h_unit = "px"; + } else if (doc_h_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_h_unit = "px"; + doc_h = dt->getDocument()->getRoot()->viewBox.height(); + } + _page_sizer.setDim(Inkscape::Util::Quantity(doc_w, doc_w_unit), Inkscape::Util::Quantity(doc_h, doc_h_unit)); + _page_sizer.updateFitMarginsUI(nv->getRepr()); + _page_sizer.updateScaleUI(); + + //-----------------------------------------------------------guide page + + _rcb_sgui.setActive (nv->showguides); + _rcb_lgui.setActive (nv->lockguides); + _rcp_gui.setRgba32 (nv->guidecolor); + _rcp_hgui.setRgba32 (nv->guidehicolor); + + //-----------------------------------------------------------snap page + + _rsu_sno.setValue (nv->snap_manager.snapprefs.getObjectTolerance()); + _rsu_sn.setValue (nv->snap_manager.snapprefs.getGridTolerance()); + _rsu_gusn.setValue (nv->snap_manager.snapprefs.getGuideTolerance()); + _rcb_snclp.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP)); + _rcb_snmsk.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK)); + _rcb_perp.setActive (nv->snap_manager.snapprefs.getSnapPerp()); + _rcb_tang.setActive (nv->snap_manager.snapprefs.getSnapTang()); + + //-----------------------------------------------------------grids page + + update_gridspage(); + + //------------------------------------------------Color Management page + +#if defined(HAVE_LIBLCMS2) + populate_linked_profiles_box(); + populate_available_profiles(); +#endif // defined(HAVE_LIBLCMS2) + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + + _wr.setUpdating (false); +} + +// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file? +Gtk::HBox& +DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 0)); + _tab_label_box->set_spacing(4); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +//-------------------------------------------------------------------- + +void DocumentProperties::on_response (int id) +{ + if (id == Gtk::RESPONSE_DELETE_EVENT || id == Gtk::RESPONSE_CLOSE) + { + _rcp_bg.closeWindow(); + _rcp_bord.closeWindow(); + _rcp_gui.closeWindow(); + _rcp_hgui.closeWindow(); + } + + if (id == Gtk::RESPONSE_CLOSE) + hide(); +} + +void DocumentProperties::load_default_metadata() +{ + /* Get the data RDF entities data from preferences*/ + for (auto & it : _rdflist) { + it->load_from_preferences (); + } +} + +void DocumentProperties::save_default_metadata() +{ + /* Save these RDF entities to preferences*/ + for (auto & it : _rdflist) { + it->save_to_preferences (SP_ACTIVE_DOCUMENT); + } +} + + +void DocumentProperties::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = document->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->removeListenerByData(this); +} + +static void on_child_added(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast(data)) + dialog->update_gridspage(); +} + +static void on_child_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast(data)) + dialog->update_gridspage(); +} + + + +/** + * Called when XML node attribute changed; updates dialog widgets. + */ +static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data) +{ + if (DocumentProperties *dialog = static_cast(data)) + dialog->update(); +} + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void DocumentProperties::onNewGrid() +{ + SPDesktop *dt = getDesktop(); + Inkscape::XML::Node *repr = dt->getNamedView()->getRepr(); + SPDocument *doc = dt->getDocument(); + + Glib::ustring typestring = _grids_combo_gridtype.get_active_text(); + CanvasGrid::writeNewGridToRepr(repr, doc, CanvasGrid::getGridTypeFromName(typestring.c_str())); + + // toggle grid showing to ON: + dt->showGrids(true); +} + + +void DocumentProperties::onRemoveGrid() +{ + gint pagenum = _grids_notebook.get_current_page(); + if (pagenum == -1) // no pages + return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + Inkscape::CanvasGrid * found_grid = nullptr; + if( pagenum < (gint)nv->grids.size()) + found_grid = nv->grids[pagenum]; + + if (found_grid) { + // delete the grid that corresponds with the selected tab + // when the grid is deleted from SVG, the SPNamedview handler automatically deletes the object, so found_grid becomes an invalid pointer! + found_grid->repr->parent()->removeChild(found_grid->repr); + DocumentUndo::done(dt->getDocument(), SP_VERB_DIALOG_NAMEDVIEW, _("Remove grid")); + } +} + +/** Callback for document unit change. */ +/* This should not effect anything in the SVG tree (other than "inkscape:document-units"). + This should only effect values displayed in the GUI. */ +void DocumentProperties::onDocUnitChange() +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + // Don't execute when change is being undone + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + // Don't execute when initializing widgets + if (_wr.isUpdating()) { + return; + } + + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + /*Inkscape::Util::Unit const *old_doc_unit = unit_table.getUnit("px"); + if(repr->attribute("inkscape:document-units")) { + old_doc_unit = unit_table.getUnit(repr->attribute("inkscape:document-units")); + }*/ + Inkscape::Util::Unit const *doc_unit = _rum_deflt.getUnit(); + + // Set document unit + Inkscape::SVGOStringStream os; + os << doc_unit->abbr; + repr->setAttribute("inkscape:document-units", os.str()); + + _page_sizer.updateScaleUI(); + + // Disable changing of SVG Units. The intent here is to change the units in the UI, not the units in SVG. + // This code should be moved (and fixed) once we have an "SVG Units" setting that sets what units are used in SVG data. +#if 0 + // Set viewBox + if (doc->getRoot()->viewBox_set) { + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->setViewBox(doc->getRoot()->viewBox*Geom::Scale(scale)); + } else { + Inkscape::Util::Quantity width = doc->getWidth(); + Inkscape::Util::Quantity height = doc->getHeight(); + doc->setViewBox(Geom::Rect::from_xywh(0, 0, width.value(doc_unit), height.value(doc_unit))); + } + + // TODO: Fix bug in nodes tool instead of switching away from it + if (tools_active(getDesktop()) == TOOLS_NODES) { + tools_switch(getDesktop(), TOOLS_SELECT); + } + + // Scale and translate objects + // set transform options to scale all things with the transform, so all things scale properly after the viewbox change. + /// \todo this "low-level" code of changing viewbox/unit should be moved somewhere else + + // save prefs + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + { + ShapeEditor::blockSetItem(true); + gdouble viewscale = 1.0; + Geom::Rect vb = doc->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = doc->getWidth().value("px") / vb.width(); + gdouble viewscale_h = doc->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(-viewscale*doc->getRoot()->viewBox.min()[Geom::X] + + (doc->getWidth().value("px") - viewscale*doc->getRoot()->viewBox.width())/2, + viewscale*doc->getRoot()->viewBox.min()[Geom::Y] + + (doc->getHeight().value("px") + viewscale*doc->getRoot()->viewBox.height())/2), + false); + ShapeEditor::blockSetItem(false); + } + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); +#endif + + doc->setModifiedSinceSave(); + + DocumentUndo::done(doc, SP_VERB_NONE, _("Changed default display unit")); +} + +} // 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 : diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h new file mode 100644 index 0000000..8b4e088 --- /dev/null +++ b/src/ui/dialog/document-properties.h @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Properties dialog + */ +/* Authors: + * Ralf Stephan + * Bryce W. Harrington + * + * Copyright (C) 2006-2008 Johan Engelen + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "ui/widget/page-sizer.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "ui/widget/tolerance-slider.h" +#include "ui/widget/panel.h" +#include "ui/widget/licensor.h" + +#include "xml/helper-observer.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + class NotebookPage; + } + namespace Dialog { + +typedef std::list RDElist; + +class DocumentProperties : public UI::Widget::Panel { +public: + void update(); + static DocumentProperties &getInstance(); + static void destroy(); + + void update_gridspage(); + +protected: + void build_page(); + void build_grid(); + void build_guides(); + void build_snap(); + void build_gridspage(); + + void create_guides_around_page(); + void delete_all_guides(); +#if defined(HAVE_LIBLCMS2) + void build_cms(); +#endif // defined(HAVE_LIBLCMS2) + void build_scripting(); + void build_metadata(); + void init(); + + virtual void on_response (int); +#if defined(HAVE_LIBLCMS2) + void populate_available_profiles(); + void populate_linked_profiles_box(); + void linkSelectedProfile(); + void removeSelectedProfile(); + void onColorProfileSelectRow(); + void linked_profiles_list_button_release(GdkEventButton* event); + void cms_create_popup_menu(Gtk::Widget& parent, sigc::slot rem); +#endif // defined(HAVE_LIBLCMS2) + + void external_scripts_list_button_release(GdkEventButton* event); + void embedded_scripts_list_button_release(GdkEventButton* event); + void populate_script_lists(); + void addExternalScript(); + void browseExternalScript(); + void addEmbeddedScript(); + void removeExternalScript(); + void removeEmbeddedScript(); + void changeEmbeddedScript(); + void onExternalScriptSelectRow(); + void onEmbeddedScriptSelectRow(); + void editEmbeddedScript(); + void external_create_popup_menu(Gtk::Widget& parent, sigc::slot rem); + void embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot rem); + void load_default_metadata(); + void save_default_metadata(); + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + Inkscape::XML::SignalObserver _emb_profiles_observer, _scripts_observer; + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_page; + UI::Widget::NotebookPage *_page_guides; + UI::Widget::NotebookPage *_page_snap; + UI::Widget::NotebookPage *_page_cms; + UI::Widget::NotebookPage *_page_scripting; + + Gtk::Notebook _scripting_notebook; + UI::Widget::NotebookPage *_page_external_scripts; + UI::Widget::NotebookPage *_page_embedded_scripts; + + UI::Widget::NotebookPage *_page_metadata1; + UI::Widget::NotebookPage *_page_metadata2; + + Gtk::VBox _grids_vbox; + + UI::Widget::Registry _wr; + //--------------------------------------------------------------- + Gtk::Grid _rcb_doc_props_left; + Gtk::Grid _rcb_doc_props_right; + UI::Widget::RegisteredCheckButton _rcb_antialias; + UI::Widget::RegisteredCheckButton _rcb_checkerboard; + UI::Widget::RegisteredCheckButton _rcb_canb; + UI::Widget::RegisteredCheckButton _rcb_bord; + UI::Widget::RegisteredCheckButton _rcb_shad; + UI::Widget::RegisteredColorPicker _rcp_bg; + UI::Widget::RegisteredColorPicker _rcp_bord; + UI::Widget::RegisteredUnitMenu _rum_deflt; + UI::Widget::PageSizer _page_sizer; + //--------------------------------------------------------------- + UI::Widget::RegisteredCheckButton _rcb_sgui; + UI::Widget::RegisteredCheckButton _rcb_lgui; + UI::Widget::RegisteredColorPicker _rcp_gui; + UI::Widget::RegisteredColorPicker _rcp_hgui; + Gtk::Button _create_guides_btn; + Gtk::Button _delete_guides_btn; + //--------------------------------------------------------------- + UI::Widget::ToleranceSlider _rsu_sno; + UI::Widget::ToleranceSlider _rsu_sn; + UI::Widget::ToleranceSlider _rsu_gusn; + UI::Widget::RegisteredCheckButton _rcb_snclp; + UI::Widget::RegisteredCheckButton _rcb_snmsk; + UI::Widget::RegisteredCheckButton _rcb_perp; + UI::Widget::RegisteredCheckButton _rcb_tang; + //--------------------------------------------------------------- + Gtk::Button _link_btn; + Gtk::Button _unlink_btn; + class AvailableProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + AvailableProfilesColumns() + { add(fileColumn); add(nameColumn); add(separatorColumn); } + Gtk::TreeModelColumn fileColumn; + Gtk::TreeModelColumn nameColumn; + Gtk::TreeModelColumn separatorColumn; + }; + AvailableProfilesColumns _AvailableProfilesListColumns; + Glib::RefPtr _AvailableProfilesListStore; + Gtk::ComboBox _AvailableProfilesList; + bool _AvailableProfilesList_separator(const Glib::RefPtr& model, const Gtk::TreeModel::iterator& iter); + class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + LinkedProfilesColumns() + { add(nameColumn); add(previewColumn); } + Gtk::TreeModelColumn nameColumn; + Gtk::TreeModelColumn previewColumn; + }; + LinkedProfilesColumns _LinkedProfilesListColumns; + Glib::RefPtr _LinkedProfilesListStore; + Gtk::TreeView _LinkedProfilesList; + Gtk::ScrolledWindow _LinkedProfilesListScroller; + Gtk::Menu _EmbProfContextMenu; + + //--------------------------------------------------------------- + Gtk::Button _external_add_btn; + Gtk::Button _external_remove_btn; + Gtk::Button _embed_new_btn; + Gtk::Button _embed_remove_btn; + Gtk::ButtonBox _embed_button_box; + + class ExternalScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ExternalScriptsColumns() + { add(filenameColumn); } + Gtk::TreeModelColumn filenameColumn; + }; + ExternalScriptsColumns _ExternalScriptsListColumns; + class EmbeddedScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + EmbeddedScriptsColumns() + { add(idColumn); } + Gtk::TreeModelColumn idColumn; + }; + EmbeddedScriptsColumns _EmbeddedScriptsListColumns; + Glib::RefPtr _ExternalScriptsListStore; + Glib::RefPtr _EmbeddedScriptsListStore; + Gtk::TreeView _ExternalScriptsList; + Gtk::TreeView _EmbeddedScriptsList; + Gtk::ScrolledWindow _ExternalScriptsListScroller; + Gtk::ScrolledWindow _EmbeddedScriptsListScroller; + Gtk::Menu _ExternalScriptsContextMenu; + Gtk::Menu _EmbeddedScriptsContextMenu; + Gtk::Entry _script_entry; + Gtk::TextView _EmbeddedContent; + Gtk::ScrolledWindow _EmbeddedContentScroller; + //--------------------------------------------------------------- + + Gtk::Notebook _grids_notebook; + Gtk::HBox _grids_hbox_crea; + Gtk::Label _grids_label_crea; + Gtk::Button _grids_button_new; + Gtk::Button _grids_button_remove; + Gtk::ComboBoxText _grids_combo_gridtype; + Gtk::Label _grids_label_def; + Gtk::HBox _grids_space; + //--------------------------------------------------------------- + + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + Gtk::HBox& _createPageTabLabel(const Glib::ustring& label, const char *label_image); + +private: + DocumentProperties(); + ~DocumentProperties() override; + + // callback methods for buttons on grids page. + void onNewGrid(); + void onRemoveGrid(); + + // callback for document unit change + void onDocUnitChange(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H + +/* + 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 : diff --git a/src/ui/dialog/export.cpp b/src/ui/dialog/export.cpp new file mode 100644 index 0000000..7d68559 --- /dev/null +++ b/src/ui/dialog/export.cpp @@ -0,0 +1,1948 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Peter Bostrom + * Jon A. Cruz + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 1999-2007, 2012 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// This has to be included prior to anything that includes setjmp.h, it croaks otherwise +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "document-undo.h" +#include "document.h" +#include "file.h" +#include "inkscape.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "verbs.h" + +// required to set status message after export +#include "desktop.h" +#include "message-stack.h" + +#include "helper/png-write.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog-events.h" +#include "ui/interface.h" +#include "ui/widget/unit-menu.h" + +#include "extension/db.h" +#include "extension/output.h" + + +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +#define SP_EXPORT_MIN_SIZE 1.0 + +#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px") + +#define EXPORT_COORD_PRECISION 3 + +#include "export.h" + +using Inkscape::Util::unit_table; + +namespace { + +class MessageCleaner +{ +public: + MessageCleaner(Inkscape::MessageId messageId, SPDesktop *desktop) : + _desktop(desktop), + _messageId(messageId) + { + } + + ~MessageCleaner() + { + if (_messageId && _desktop) { + _desktop->messageStack()->cancel(_messageId); + } + } + +private: + MessageCleaner(MessageCleaner const &other) = delete; + MessageCleaner &operator=(MessageCleaner const &other) = delete; + + SPDesktop *_desktop; + Inkscape::MessageId _messageId; +}; + +} // namespace + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** A list of strings that is used both in the preferences, and in the + data fields to describe the various values of \c selection_type. */ +static const char * selection_names[SELECTION_NUMBER_OF] = { + "page", "drawing", "selection", "custom" +}; + +/** The names on the buttons for the various selection types. */ +static const char * selection_labels[SELECTION_NUMBER_OF] = { + N_("_Page"), N_("_Drawing"), N_("_Selection"), N_("_Custom") +}; + +Export::Export () : + UI::Widget::Panel("/dialogs/export/", SP_VERB_DIALOG_EXPORT), + current_key(SELECTION_PAGE), + original_name(), + doc_export_name(), + filename_modified(false), + was_empty(true), + update(false), + togglebox(true, 0), + area_box(false, 3), + singleexport_box(false, 0), + size_box(false, 3), + file_box(false, 3), + unitbox(false, 0), + unit_selector(), + units_label(_("Units:")), + filename_box(false, 5), + browse_label(_("_Export As..."), true), + browse_image(), + batch_box(false, 5), + batch_export(_("B_atch export all selected objects")), + interlacing(_("Use interlacing")), + bitdepth_label(_("Bit depth")), + bitdepth_cb(), + zlib_label(_("Compression")), + zlib_compression(), + pHYs_label(_("pHYs dpi")), + pHYs_sb(pHYs_adj, 1.0, 2), + antialiasing_label(_("Antialiasing")), + antialiasing_cb(), + hide_box(false, 3), + hide_export(_("Hide all except selected")), + closeWhenDone(_("Close when complete")), + button_box(false, 3), + _prog(), + prog_dlg(nullptr), + interrupted(false), + prefs(nullptr), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + batch_export.set_use_underline(); + batch_export.set_tooltip_text(_("Export each selected object into its own PNG file, using export hints if any (caution, overwrites without asking!)")); + hide_export.set_use_underline(); + hide_export.set_tooltip_text(_("In the exported image, hide all objects except those that are selected")); + interlacing.set_use_underline(); + interlacing.set_tooltip_text(_("Enables ADAM7 interlacing for PNG output. This results in slightly heavier images, but big images will look better sooner when loading the file")); + closeWhenDone.set_use_underline(); + closeWhenDone.set_tooltip_text(_("Once the export completes, close this dialog")); + prefs = Inkscape::Preferences::get(); + + singleexport_box.set_border_width(0); + + /* Export area frame */ + { + Gtk::Label* lbl = new Gtk::Label(_("Export area"), Gtk::ALIGN_START); + lbl->set_use_markup(true); + area_box.pack_start(*lbl); + + /* Units box */ + /* gets added to the vbox later, but the unit selector is needed + earlier than that */ + unit_selector.setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + unit_selector.setUnit(desktop->getNamedView()->display_units->abbr); + } + unitChangedConn = unit_selector.signal_changed().connect(sigc::mem_fun(*this, &Export::onUnitChanged)); + unitbox.pack_end(unit_selector, false, false, 0); + unitbox.pack_end(units_label, false, false, 3); + + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + selectiontype_buttons[i] = new Gtk::RadioButton(_(selection_labels[i]), true); + if (i > 0) { + Gtk::RadioButton::Group group = selectiontype_buttons[0]->get_group(); + selectiontype_buttons[i]->set_group(group); + } + selectiontype_buttons[i]->set_mode(false); + togglebox.pack_start(*selectiontype_buttons[i], false, true, 0); + selectiontype_buttons[i]->signal_clicked().connect(sigc::mem_fun(*this, &Export::onAreaToggled)); + } + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + x0_adj = createSpinbutton ( "x0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 0, _("_x0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX0Change); + + x1_adj = createSpinbutton ( "x1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 1, _("x_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX1Change); + + width_adj = createSpinbutton ( "width", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 0, 2, _("Wid_th:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaWidthChange); + + y0_adj = createSpinbutton ( "y0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 0, _("_y0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY0Change); + + y1_adj = createSpinbutton ( "y1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 1, _("y_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY1Change); + + height_adj = createSpinbutton ( "height", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 2, 2, _("Hei_ght:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaHeightChange); + + area_box.pack_start(togglebox, false, false, 3); + area_box.pack_start(*t, false, false, 0); + area_box.pack_start(unitbox, false, false, 0); + + area_box.set_border_width(3); + singleexport_box.pack_start(area_box, false, false, 0); + + } // end of area box + + /* Bitmap size frame */ + { + size_box.set_border_width(3); + bm_label = new Gtk::Label(_("Image size"), Gtk::ALIGN_START); + bm_label->set_use_markup(true); + size_box.pack_start(*bm_label, false, false, 0); + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + size_box.pack_start(*t); + + bmwidth_adj = createSpinbutton ( "bmwidth", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 0, + _("_Width:"), _("pixels at"), 0, 1, + &Export::onBitmapWidthChange); + + xdpi_adj = createSpinbutton ( "xdpi", + prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 0, + "", _("dp_i"), 2, 1, + &Export::onExportXdpiChange); + + bmheight_adj = createSpinbutton ( "bmheight", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 1, + _("_Height:"), _("pixels at"), 0, 1, + &Export::onBitmapHeightChange); + + /** TODO + * There's no way to set ydpi currently, so we use the defaultxdpi value here, too... + */ + ydpi_adj = createSpinbutton ( "ydpi", prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 1, + "", _("dpi"), 2, 0, nullptr ); + + singleexport_box.pack_start(size_box, Gtk::PACK_SHRINK); + } + + /* File entry */ + { + file_box.set_border_width(3); + flabel = new Gtk::Label(_("_Filename"), Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + flabel->set_use_markup(true); + file_box.pack_start(*flabel, false, false, 0); + + set_default_filename(); + + filename_box.pack_start (filename_entry, true, true, 0); + + Gtk::HBox* browser_im_label = new Gtk::HBox(false, 3); + browse_image.set_from_icon_name("folder", Gtk::ICON_SIZE_BUTTON); + browser_im_label->pack_start(browse_image); + browser_im_label->pack_start(browse_label); + browse_button.add(*browser_im_label); + filename_box.pack_end (browse_button, false, false); + + file_box.add(filename_box); + + original_name = filename_entry.get_text(); + + // focus is in the filename initially: + filename_entry.grab_focus(); + + // mnemonic in frame label moves focus to filename: + flabel->set_mnemonic_widget(filename_entry); + + singleexport_box.pack_start(file_box, Gtk::PACK_SHRINK); + } + + batch_export.set_sensitive(true); + batch_box.pack_start(batch_export, false, false, 3); + + hide_export.set_sensitive(true); + hide_export.set_active (prefs->getBool("/dialogs/export/hideexceptselected/value", false)); + hide_box.pack_start(hide_export, false, false, 3); + + + /* Export Button row */ + export_button.set_label(_("_Export")); + export_button.set_use_underline(); + export_button.set_tooltip_text (_("Export the bitmap file with these settings")); + + button_box.set_border_width(3); + button_box.pack_start(closeWhenDone, true, true, 0); + button_box.pack_end(export_button, false, false, 0); + + /*Advanced*/ + Gtk::Label *label_advanced = Gtk::manage(new Gtk::Label(_("Advanced"),true)); + expander.set_label_widget(*label_advanced); + expander.set_vexpand(false); + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + for(auto i : modes_list) + bitdepth_cb.append(i); + bitdepth_cb.set_active_text("RGBA_8"); + bitdepth_cb.set_hexpand(); + const char* const zlist[]={"Z_NO_COMPRESSION","Z_BEST_SPEED","2","3","4","5","Z_DEFAULT_COMPRESSION","7","8","Z_BEST_COMPRESSION"}; + for(auto i : zlist) + zlib_compression.append(i); + zlib_compression.set_active_text("Z_DEFAULT_COMPRESSION"); + pHYs_adj = Gtk::Adjustment::create(0, 0, 100000, 0.1, 1.0, 0); + pHYs_sb.set_adjustment(pHYs_adj); + pHYs_sb.set_width_chars(7); + pHYs_sb.set_tooltip_text( _("Will force-set the physical dpi for the png file. Set this to 72 if you're planning to work on your png with Photoshop") ); + zlib_compression.set_hexpand(); + const char* const antialising_list[] = {"CAIRO_ANTIALIAS_NONE","CAIRO_ANTIALIAS_FAST","CAIRO_ANTIALIAS_GOOD (default)","CAIRO_ANTIALIAS_BEST"}; + for(auto i : antialising_list) + antialiasing_cb.append(i); + antialiasing_cb.set_active_text(antialising_list[2]); + bitdepth_label.set_halign(Gtk::ALIGN_START); + zlib_label.set_halign(Gtk::ALIGN_START); + pHYs_label.set_halign(Gtk::ALIGN_START); + antialiasing_label.set_halign(Gtk::ALIGN_START); + auto table = new Gtk::Grid(); + expander.add(*table); + // gtk_container_add(GTK_CONTAINER(expander.gobj()), (GtkWidget*)(table->gobj())); + table->set_border_width(4); + table->attach(interlacing,0,0,1,1); + table->attach(bitdepth_label,0,1,1,1); + table->attach(bitdepth_cb,1,1,1,1); + table->attach(zlib_label,0,2,1,1); + table->attach(zlib_compression,1,2,1,1); + table->attach(pHYs_label,0,3,1,1); + table->attach(pHYs_sb,1,3,1,1); + table->attach(antialiasing_label,0,4,1,1); + table->attach(antialiasing_cb,1,4,1,1); + table->show(); + + /* Main dialog */ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + contents->pack_start(singleexport_box, Gtk::PACK_SHRINK); + contents->pack_start(batch_box, Gtk::PACK_SHRINK); + contents->pack_start(hide_box, Gtk::PACK_SHRINK); + contents->pack_start(expander, Gtk::PACK_SHRINK); + contents->pack_end(button_box, Gtk::PACK_SHRINK); + contents->pack_end(_prog, Gtk::PACK_SHRINK); + + /* Signal handlers */ + filename_entry.signal_changed().connect( sigc::mem_fun(*this, &Export::onFilenameModified) ); + // pressing enter in the filename field is the same as clicking export: + filename_entry.signal_activate().connect(sigc::mem_fun(*this, &Export::onExport) ); + browse_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBrowse)); + batch_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBatchClicked)); + export_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onExport)); + hide_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onHideExceptSelected)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Export::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + setExporting(false); + + findDefaultSelection(); + onAreaToggled(); +} + +Export::~Export () +{ + was_empty = TRUE; + + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void Export::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Export::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + + //// Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &Export::onSelectionModified))); + } + } +} + +/* + * set the default filename to be that of the current path + document + * with .png extension + * + * One thing to notice here is that this filename may get + * overwritten, but it won't happen here. The filename gets + * written into the text field, but then the button to select + * the area gets set. In that code the filename can be changed + * if there are some with presidence in the document. So, while + * this code sets the name first, it may not be the one users + * really see. + */ +void Export::set_default_filename () { + + if ( SP_ACTIVE_DOCUMENT && SP_ACTIVE_DOCUMENT->getDocumentURI() ) + { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + const gchar *uri = doc->getDocumentURI(); + auto &&text_extension = get_file_save_extension(Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + Inkscape::Extension::Output * oextension = nullptr; + + if (!text_extension.empty()) { + oextension = dynamic_cast(Inkscape::Extension::db.get(text_extension.c_str())); + } + + if (oextension != nullptr) { + gchar * old_extension = oextension->get_extension(); + if (g_str_has_suffix(uri, old_extension)) { + gchar * uri_copy; + gchar * extension_point; + gchar * final_name; + + uri_copy = g_strdup(uri); + extension_point = g_strrstr(uri_copy, old_extension); + extension_point[0] = '\0'; + + final_name = g_strconcat(uri_copy, ".png", NULL); + filename_entry.set_text(final_name); + filename_entry.set_position(strlen(final_name)); + + g_free(final_name); + g_free(uri_copy); + } + } else { + gchar *name = g_strconcat(uri, ".png", NULL); + filename_entry.set_text(name); + filename_entry.set_position(strlen(name)); + + g_free(name); + } + + doc_export_name = filename_entry.get_text(); + } + else if ( SP_ACTIVE_DOCUMENT ) + { + Glib::ustring filename = create_filepath_from_id (_("bitmap"), filename_entry.get_text()); + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + + doc_export_name = filename_entry.get_text(); + } +} + +Glib::RefPtr Export::createSpinbutton( gchar const * /*key*/, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ) +{ + auto adj = Gtk::Adjustment::create(val, min, max, step, page, 0); + + int pos = 0; + Gtk::Label *l = nullptr; + + if (!ll.empty()) { + l = new Gtk::Label(ll,true); + l->set_halign(Gtk::ALIGN_END); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive(sensitive); + pos++; + } + + auto sb = new Gtk::SpinButton(adj, 1.0, digits); + sb->set_hexpand(); + t->attach(*sb, x + pos, y, 1, 1); + + sb->set_width_chars(7); + sb->set_sensitive (sensitive); + pos++; + + if (l) { + l->set_mnemonic_widget(*sb); + } + + if (!lr.empty()) { + l = new Gtk::Label(lr,true); + l->set_halign(Gtk::ALIGN_START); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive (sensitive); + pos++; + l->set_mnemonic_widget (*sb); + } + + if (cb) { + adj->signal_value_changed().connect( sigc::mem_fun(*this, cb) ); + } + + return adj; +} // end of createSpinbutton() + + +Glib::ustring Export::create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text) +{ + if (id.empty()) + { /* This should never happen */ + id = "bitmap"; + } + + Glib::ustring directory; + + if (!file_entry_text.empty()) { + directory = Glib::path_get_dirname(file_entry_text); + } + + if (directory.empty()) { + /* Grab document directory */ + const gchar* docURI = SP_ACTIVE_DOCUMENT->getDocumentURI(); + if (docURI) { + directory = Glib::path_get_dirname(docURI); + } + } + + if (directory.empty()) { + directory = Inkscape::IO::Resource::homedir_path(nullptr); + } + + Glib::ustring filename = Glib::build_filename(directory, id+".png"); + return filename; +} + +void Export::onBatchClicked () +{ + if (batch_export.get_active()) { + singleexport_box.set_sensitive(false); + } else { + singleexport_box.set_sensitive(true); + } +} + +void Export::updateCheckbuttons () +{ + gint num = (gint) boost::distance(SP_ACTIVE_DESKTOP->getSelection()->items()); + if (num >= 2) { + batch_export.set_sensitive(true); + batch_export.set_label(g_strdup_printf (ngettext("B_atch export %d selected object","B_atch export %d selected objects",num), num)); + } else { + batch_export.set_active (false); + batch_export.set_sensitive(false); + } + + //hide_export.set_sensitive (num > 0); +} + +inline void Export::findDefaultSelection() +{ + selection_type key = SELECTION_NUMBER_OF; + + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + key = SELECTION_SELECTION; + } + + /* Try using the preferences */ + if (key == SELECTION_NUMBER_OF) { + + int i = SELECTION_NUMBER_OF; + + Glib::ustring what = prefs->getString("/dialogs/export/exportarea/value"); + + if (!what.empty()) { + for (i = 0; i < SELECTION_NUMBER_OF; i++) { + if (what == selection_names[i]) { + break; + } + } + } + + key = (selection_type)i; + } + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_SELECTION; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + updateCheckbuttons (); +} + + +/** + * If selection changed or a different document activated, we must + * recalculate any chosen areas. + */ +void Export::onSelectionChanged() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + + if ((current_key == SELECTION_DRAWING || current_key == SELECTION_PAGE) && + (SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false && + was_empty) { + current_key = SELECTION_SELECTION; + selectiontype_buttons[current_key]->set_active(true); + } + was_empty = (SP_ACTIVE_DESKTOP->getSelection())->isEmpty(); + + if ( selection && + SELECTION_CUSTOM != current_key) { + onAreaToggled(); + } + + updateCheckbuttons (); +} + +void Export::onSelectionModified ( guint /*flags*/ ) +{ + Inkscape::Selection * Sel; + switch (current_key) { + case SELECTION_DRAWING: + if ( SP_ACTIVE_DESKTOP ) { + SPDocument *doc; + doc = SP_ACTIVE_DESKTOP->getDocument(); + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + if (bbox) { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + case SELECTION_SELECTION: + Sel = SP_ACTIVE_DESKTOP->getSelection(); + if (Sel->isEmpty() == false) { + Geom::OptRect bbox = Sel->visualBounds(); + if (bbox) + { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + default: + /* Do nothing for page or for custom */ + break; + } + + return; +} + +/// Called when one of the selection buttons was toggled. +void Export::onAreaToggled () +{ + if (update) { + return; + } + + /* Find which button is active */ + selection_type key = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + if (selectiontype_buttons[i]->get_active()) { + key = (selection_type)i; + } + } + + if ( SP_ACTIVE_DESKTOP ) + { + SPDocument *doc; + Geom::OptRect bbox; + bbox = Geom::Rect(Geom::Point(0.0, 0.0),Geom::Point(0.0, 0.0)); + doc = SP_ACTIVE_DESKTOP->getDocument(); + + /* Notice how the switch is used to 'fall through' here to get + various backups. If you modify this without noticing you'll + probably screw something up. */ + switch (key) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) + { + bbox = SP_ACTIVE_DESKTOP->getSelection()->visualBounds(); + /* Only if there is a selection that we can set + do we break, otherwise we fall through to the + drawing */ + // std::cout << "Using selection: SELECTION" << std::endl; + key = SELECTION_SELECTION; + break; + } + case SELECTION_DRAWING: + /** \todo + * This returns wrong values if the document has a viewBox. + */ + bbox = doc->getRoot()->desktopVisualBounds(); + /* If the drawing is valid, then we'll use it and break + otherwise we drop through to the page settings */ + if (bbox) { + // std::cout << "Using selection: DRAWING" << std::endl; + key = SELECTION_DRAWING; + break; + } + case SELECTION_PAGE: + bbox = Geom::Rect(Geom::Point(0.0, 0.0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + + // std::cout << "Using selection: PAGE" << std::endl; + key = SELECTION_PAGE; + break; + case SELECTION_CUSTOM: + default: + break; + } // switch + + current_key = key; + + // remember area setting + prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]); + + if ( key != SELECTION_CUSTOM && bbox ) { + setArea ( bbox->min()[Geom::X], + bbox->min()[Geom::Y], + bbox->max()[Geom::X], + bbox->max()[Geom::Y]); + } + + } // end of if ( SP_ACTIVE_DESKTOP ) + + if (SP_ACTIVE_DESKTOP && !filename_modified) { + + Glib::ustring filename; + float xdpi = 0.0, ydpi = 0.0; + + switch (key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + sp_document_get_export_hints (doc, filename, &xdpi, &ydpi); + + if (filename.empty()) { + if (!doc_export_name.empty()) { + filename = doc_export_name; + } + } + break; + } + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + + SP_ACTIVE_DESKTOP->getSelection()->getExportHints(filename, &xdpi, &ydpi); + + /* If we still don't have a filename -- let's build + one that's nice */ + if (filename.empty()) { + const gchar * id = "object"; + auto reprlst = SP_ACTIVE_DESKTOP->getSelection()->xmlNodes(); + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + if (repr->attribute("id")) { + id = repr->attribute("id"); + break; + } + } + + filename = create_filepath_from_id (id, filename_entry.get_text()); + } + } + break; + case SELECTION_CUSTOM: + default: + break; + } + + if (!filename.empty()) { + original_name = filename; + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + } + + if (xdpi != 0.0) { + setValue(xdpi_adj, xdpi); + } + + /* These can't be separate, and setting x sets y, so for + now setting this is disabled. Hopefully it won't be in + the future */ + if (FALSE && ydpi != 0.0) { + setValue(ydpi_adj, ydpi); + } + } + + return; +} // end of sp_export_area_toggled() + +/// Called when dialog is deleted +bool Export::onProgressDelete (GdkEventAny * /*event*/) +{ + interrupted = true; + return TRUE; +} // end of sp_export_progress_delete() + + +/// Called when progress is cancelled +void Export::onProgressCancel () +{ + interrupted = true; +} // end of sp_export_progress_cancel() + + +/// Called for every progress iteration +unsigned int Export::onProgressCallback(float value, void *dlg) +{ + Gtk::Dialog *dlg2 = reinterpret_cast(dlg); + + Export *self = reinterpret_cast(dlg2->get_data("exportPanel")); + if (self->interrupted) + return FALSE; + + gint current = GPOINTER_TO_INT(dlg2->get_data("current")); + gint total = GPOINTER_TO_INT(dlg2->get_data("total")); + if (total > 0) { + double completed = current; + completed /= static_cast(total); + + value = completed + (value / static_cast(total)); + } + + Gtk::ProgressBar *prg = reinterpret_cast(dlg2->get_data("progress")); + prg->set_fraction(value); + + if (self) { + self->_prog.set_fraction(value); + } + + int evtcount = 0; + while ((evtcount < 16) && gdk_events_pending()) { + gtk_main_iteration_do(FALSE); + evtcount += 1; + } + + gtk_main_iteration_do(FALSE); + return TRUE; +} // end of sp_export_progress_callback() + +void Export::setExporting(bool exporting, Glib::ustring const &text) +{ + if (exporting) { + _prog.set_text(text); + _prog.set_fraction(0.0); + _prog.set_sensitive(true); + + export_button.set_sensitive(false); + } else { + _prog.set_text(""); + _prog.set_fraction(0.0); + _prog.set_sensitive(false); + + export_button.set_sensitive(true); + } +} + +Gtk::Dialog * Export::create_progress_dialog (Glib::ustring progress_text) { + Gtk::Dialog *dlg = new Gtk::Dialog(_("Export in progress"), TRUE); + dlg->set_transient_for( *(INKSCAPE.active_desktop()->getToplevel()) ); + + Gtk::ProgressBar *prg = new Gtk::ProgressBar (); + prg->set_text(progress_text); + dlg->set_data ("progress", prg); + auto CA = dlg->get_content_area(); + CA->pack_start(*prg, FALSE, FALSE, 4); + + Gtk::Button* btn = dlg->add_button (_("_Cancel"),Gtk::RESPONSE_CANCEL ); + + btn->signal_clicked().connect( sigc::mem_fun(*this, &Export::onProgressCancel) ); + dlg->signal_delete_event().connect( sigc::mem_fun(*this, &Export::onProgressDelete) ); + + dlg->show_all (); + return dlg; +} + +// FIXME: Some lib function should be available to do this ... +Glib::ustring Export::filename_add_extension (Glib::ustring filename, Glib::ustring extension) +{ + auto pos = int(filename.size()) - int(extension.size()); + if (pos > 0 && filename[pos - 1] == '.' && filename.substr(pos).lowercase() == extension.lowercase()) { + return filename; + } + + return filename + "." + extension; +} + +Glib::ustring Export::absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename) +{ + Glib::ustring path; + //Make relative paths go from the document location, if possible: + if (!Glib::path_is_absolute(filename) && doc->getDocumentURI()) { + Glib::ustring dirname = Glib::path_get_dirname(doc->getDocumentURI()); + if (!dirname.empty()) { + path = Glib::build_filename(dirname, filename); + } + } + if (path.empty()) { + path = filename; + } + return path; +} + +// Called when unit is changed +void Export::onUnitChanged() +{ + onAreaToggled(); +} + +void Export::onHideExceptSelected () +{ + prefs->setBool("/dialogs/export/hideexceptselected/value", hide_export.get_active()); +} + +/// Called when export button is clicked +void Export::onExport () +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) return; + + SPNamedView *nv = desktop->getNamedView(); + SPDocument *doc = desktop->getDocument(); + + bool exportSuccessful = false; + + bool hide = hide_export.get_active (); + + // Advanced parameters + bool do_interlace = (interlacing.get_active()); + float pHYs = 0; + int zlib = zlib_compression.get_active_row_number() ; + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + int colortypes[] = {0,0,0,0,0,2,2,4,4,6,6}; //keep in sync with modes_list in Export constructor. values are from libpng doc. + int bitdepths[] = {1,2,4,8,16,8,16,8,16,8,16}; + int color_type = colortypes[bitdepth_cb.get_active_row_number()] ; + int bit_depth = bitdepths[bitdepth_cb.get_active_row_number()] ; + int antialiasing = antialiasing_cb.get_active_row_number(); + + + if (batch_export.get_active ()) { + // Batch export of selected objects + + gint num = (gint) boost::distance(desktop->getSelection()->items()); + gint n = 0; + + if (num < 1) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected.")); + return; + } + + prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 files"), num)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 files"), num)); + + gint export_count = 0; + + auto itemlist= desktop->getSelection()->items(); + for(auto i = itemlist.begin();i!=itemlist.end() && !interrupted ;++i){ + SPItem *item = *i; + + prog_dlg->set_data("current", GINT_TO_POINTER(n)); + prog_dlg->set_data("total", GINT_TO_POINTER(num)); + onProgressCallback(0.0, prog_dlg); + + // retrieve export filename hint + const gchar *filename = item->getRepr()->attribute("inkscape:export-filename"); + Glib::ustring path; + if (!filename) { + Glib::ustring tmp; + path = create_filepath_from_id(item->getId(), tmp); + } else { + path = absolutize_path_from_document_location(doc, filename); + } + + // retrieve export dpi hints + const gchar *dpi_hint = item->getRepr()->attribute("inkscape:export-xdpi"); // only xdpi, ydpi is always the same now + gdouble dpi = 0.0; + if (dpi_hint) { + dpi = atof(dpi_hint); + } + if (dpi == 0.0) { + dpi = getValue(xdpi_adj); + } + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : dpi; + + Geom::OptRect area = item->documentVisualBounds(); + if (area) { + gint width = (gint) (area->width() * dpi / DPI_BASE + 0.5); + gint height = (gint) (area->height() * dpi / DPI_BASE + 0.5); + + if (width > 1 && height > 1) { + // Do export + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + MessageCleaner msgCleanup(desktop->messageStack()->pushF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file %s..."), safeFile), desktop); + MessageCleaner msgFlashCleanup(desktop->messageStack()->flashF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file %s..."), safeFile), desktop); + std::vector x; + std::vector selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + if (!sp_export_png_file (doc, path.c_str(), + *area, width, height, pHYs, pHYs, + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + TRUE, // overwrite without asking + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + )) { + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flashF(Inkscape::ERROR_MESSAGE, + _("Could not export to filename %s."), safeFile); + + sp_ui_error_dialog(error); + g_free(error); + } else { + ++export_count; // one more item exported successfully + } + g_free(safeFile); + } + } + + n++; + } + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, + _("Successfully exported %d files from %d selected items."), export_count, num); + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + exportSuccessful = (export_count > 0); + } else { + Glib::ustring filename = filename_entry.get_text(); + + if (filename.empty()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename.")); + sp_ui_error_dialog(_("You have to enter a filename")); + return; + } + + float const x0 = getValuePx(x0_adj); + float const y0 = getValuePx(y0_adj); + float const x1 = getValuePx(x1_adj); + float const y1 = getValuePx(y1_adj); + float const xdpi = getValue(xdpi_adj); + float const ydpi = getValue(ydpi_adj); + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : xdpi; + unsigned long int const width = int(getValue(bmwidth_adj) + 0.5); + unsigned long int const height = int(getValue(bmheight_adj) + 0.5); + + if (!((x1 > x0) && (y1 > y0) && (width > 0) && (height > 0))) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The chosen area to be exported is invalid.")); + sp_ui_error_dialog(_("The chosen area to be exported is invalid")); + return; + } + + // make sure that .png is the extension of the file: + Glib::ustring const filename_ext = filename_add_extension(filename, "png"); + filename_entry.set_text(filename_ext); + filename_entry.set_position(filename_ext.length()); + Glib::ustring path = absolutize_path_from_document_location(doc, filename_ext); + + Glib::ustring dirname = Glib::path_get_dirname(path); + if ( dirname.empty() + || !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) ) + { + gchar *safeDir = Inkscape::IO::sanitizeString(dirname.c_str()); + gchar *error = g_strdup_printf(_("Directory %s does not exist or is not a directory.\n"), + safeDir); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeDir); + g_free(error); + return; + } + + Glib::ustring fn = path_get_basename (path); + + /* TRANSLATORS: %1 will be the filename, %2 the width, and %3 the height of the image */ + prog_dlg = create_progress_dialog (Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), fn, width, height)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), fn, width, height)); + + prog_dlg->set_data("current", GINT_TO_POINTER(0)); + prog_dlg->set_data("total", GINT_TO_POINTER(0)); + + auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * desktop->dt2doc(); + + /* Do export */ + std::vector x; + std::vector selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + ExportResult status = sp_export_png_file(desktop->getDocument(), path.c_str(), + area, width, height, pHYs, pHYs, //previously xdpi, ydpi. + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + FALSE, + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + ); + if (status == EXPORT_ERROR) { + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeFile); + g_free(error); + } else if (status == EXPORT_OK) { + exportSuccessful = true; + gchar *safeFile = Inkscape::IO::sanitizeString(path.c_str()); + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to %s."), safeFile); + + g_free(safeFile); + } else { + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted.")); + } + + /* Reset the filename so that it can be changed again by changing + selections and all that */ + original_name = filename_ext; + filename_modified = false; + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + + /* Setup the values in the document */ + switch (current_key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + Inkscape::XML::Node * repr = doc->getReprRoot(); + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + + gchar const *temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + case SELECTION_SELECTION: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + auto reprlst = desktop->getSelection()->xmlNodes(); + + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + const gchar * temp_string; + Glib::ustring dir = Glib::path_get_dirname(filename.c_str()); + const gchar* docURI=SP_ACTIVE_DOCUMENT->getDocumentURI(); + Glib::ustring docdir; + if (docURI) + { + docdir = Glib::path_get_dirname(docURI); + } + if (repr->attribute("id") == nullptr || + !(filename_ext.find_last_of(repr->attribute("id")) && + ( !docURI || + (dir == docdir)))) { + temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + default: + break; + } + } + + if (exportSuccessful && closeWhenDone.get_active()) { + for ( Gtk::Container *parent = get_parent(); parent; parent = parent->get_parent()) { + if ( GDL_IS_DOCK_ITEM(parent->gobj()) ) { + GdlDockItem *item = GDL_DOCK_ITEM(parent->gobj()); + if (item) { + gdl_dock_item_hide_item(item); + } + break; + } + } + } +} // end of sp_export_export_clicked() + +/// Called when Browse button is clicked +/// @todo refactor this code to use ui/dialogs/filedialog.cpp +void Export::onBrowse () +{ + GtkWidget *fs; + Glib::ustring filename; + + fs = gtk_file_chooser_dialog_new (_("Select a filename for exporting"), + (GtkWindow*)desktop->getToplevel(), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL ); + + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(fs), false); + + sp_transientize (fs); + + gtk_window_set_modal(GTK_WINDOW (fs), true); + + filename = filename_entry.get_text(); + + if (filename.empty()) { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + + gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (fs), filename.c_str()); + +#ifdef _WIN32 + // code in this section is borrowed from ui/dialogs/filedialogimpl-win32.cpp + OPENFILENAMEW opf; + WCHAR filter_string[20]; + wcsncpy(filter_string, L"PNG#*.png##", 11); + filter_string[3] = L'\0'; + filter_string[9] = L'\0'; + filter_string[10] = L'\0'; + WCHAR* title_string = (WCHAR*)g_utf8_to_utf16(_("Select a filename for exporting"), -1, NULL, NULL, NULL); + WCHAR* extension_string = (WCHAR*)g_utf8_to_utf16("*.png", -1, NULL, NULL, NULL); + // Copy the selected file name, converting from UTF-8 to UTF-16 + std::string dirname = Glib::path_get_dirname(filename.raw()); + if ( !Glib::file_test(dirname, Glib::FILE_TEST_EXISTS) || + Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) || + dirname.empty() ) + { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + WCHAR _filename[_MAX_PATH + 1]; + memset(_filename, 0, sizeof(_filename)); + gunichar2* utf16_path_string = g_utf8_to_utf16(filename.c_str(), -1, NULL, NULL, NULL); + wcsncpy(_filename, reinterpret_cast(utf16_path_string), _MAX_PATH); + g_free(utf16_path_string); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Glib::RefPtr parentWindow = desktop->getToplevel()->get_window(); + g_assert(parentWindow->gobj() != NULL); + + opf.hwndOwner = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); + opf.lpstrFilter = filter_string; + opf.lpstrCustomFilter = 0; + opf.nMaxCustFilter = 0L; + opf.nFilterIndex = 1L; + opf.lpstrFile = _filename; + opf.nMaxFile = _MAX_PATH; + opf.lpstrFileTitle = NULL; + opf.nMaxFileTitle=0; + opf.lpstrInitialDir = 0; + opf.lpstrTitle = title_string; + opf.nFileOffset = 0; + opf.nFileExtension = 2; + opf.lpstrDefExt = extension_string; + opf.lpfnHook = NULL; + opf.lCustData = 0; + opf.Flags = OFN_PATHMUSTEXIST; + opf.lStructSize = sizeof(OPENFILENAMEW); + if (GetSaveFileNameW(&opf) != 0) + { + // Copy the selected file name, converting from UTF-16 to UTF-8 + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)opf.lpstrFile, _MAX_PATH, NULL, NULL, NULL); + filename_entry.set_text(utf8string); + filename_entry.set_position(strlen(utf8string)); + g_free(utf8string); + + } + g_free(extension_string); + g_free(title_string); + +#else + if (gtk_dialog_run (GTK_DIALOG (fs)) == GTK_RESPONSE_ACCEPT) + { + gchar *file; + + file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (fs)); + + gchar * utf8file = g_filename_to_utf8( file, -1, nullptr, nullptr, nullptr ); + filename_entry.set_text (utf8file); + filename_entry.set_position(strlen(utf8file)); + + g_free(utf8file); + g_free(file); + } +#endif + + gtk_widget_destroy (fs); + + return; +} // end of sp_export_browse_clicked() + +// TODO: Move this to nr-rect-fns.h. +bool Export::bbox_equal(Geom::Rect const &one, Geom::Rect const &two) +{ + double const epsilon = pow(10.0, -EXPORT_COORD_PRECISION); + return ( + (fabs(one.min()[Geom::X] - two.min()[Geom::X]) < epsilon) && + (fabs(one.min()[Geom::Y] - two.min()[Geom::Y]) < epsilon) && + (fabs(one.max()[Geom::X] - two.max()[Geom::X]) < epsilon) && + (fabs(one.max()[Geom::Y] - two.max()[Geom::Y]) < epsilon) + ); +} + +/** + *This function is used to detect the current selection setting + * based on the values in the x0, y0, x1 and y0 fields. + * + * One of the most confusing parts of this function is why the array + * is built at the beginning. What needs to happen here is that we + * should always check the current selection to see if it is the valid + * one. While this is a performance improvement it is also a usability + * one during the cases where things like selections and drawings match + * size. This way buttons change less 'randomly' (at least in the eyes + * of the user). To do this an array is built where the current selection + * type is placed first, and then the others in an order from smallest + * to largest (this can be configured by reshuffling \c test_order). + * + * All of the values in this function are rounded to two decimal places + * because that is what is shown to the user. While everything is kept + * more accurate than that, the user can't control more accurate than + * that, so for this to work for them - it needs to check on that level + * of accuracy. + * + * @todo finish writing this up. + */ +void Export::detectSize() { + static const selection_type test_order[SELECTION_NUMBER_OF] = {SELECTION_SELECTION, SELECTION_DRAWING, SELECTION_PAGE, SELECTION_CUSTOM}; + selection_type this_test[SELECTION_NUMBER_OF + 1]; + selection_type key = SELECTION_NUMBER_OF; + + Geom::Point x(getValuePx(x0_adj), + getValuePx(y0_adj)); + Geom::Point y(getValuePx(x1_adj), + getValuePx(y1_adj)); + Geom::Rect current_bbox(x, y); + + this_test[0] = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + this_test[i + 1] = test_order[i]; + } + + for (int i = 0; + i < SELECTION_NUMBER_OF + 1 && + key == SELECTION_NUMBER_OF && + SP_ACTIVE_DESKTOP != nullptr; + i++) { + switch (this_test[i]) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + Geom::OptRect bbox = (SP_ACTIVE_DESKTOP->getSelection())->bounds(SPItem::VISUAL_BBOX); + + if ( bbox && bbox_equal(*bbox,current_bbox)) { + key = SELECTION_SELECTION; + } + } + break; + case SELECTION_DRAWING: { + SPDocument *doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + + if ( bbox && bbox_equal(*bbox,current_bbox) ) { + key = SELECTION_DRAWING; + } + break; + } + + case SELECTION_PAGE: { + SPDocument *doc; + + doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::Point x(0.0, 0.0); + Geom::Point y(doc->getWidth().value("px"), + doc->getHeight().value("px")); + Geom::Rect bbox(x, y); + + if (bbox_equal(bbox,current_bbox)) { + key = SELECTION_PAGE; + } + + break; + } + default: + break; + } + } + // std::cout << std::endl; + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_CUSTOM; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + + return; +} /* sp_export_detect_size */ + +/// Called when area x0 value is changed +void Export::areaXChange(Glib::RefPtr& adj) +{ + float x0, x1, xdpi, width; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + width = floor ((x1 - x0) * xdpi / DPI_BASE + 0.5); + + if (width < SP_EXPORT_MIN_SIZE) { + width = SP_EXPORT_MIN_SIZE; + + if (adj == x1_adj) { + x1 = x0 + width * DPI_BASE / xdpi; + setValuePx(x1_adj, x1); + } else { + x0 = x1 - width * DPI_BASE / xdpi; + setValuePx(x0_adj, x0); + } + } + + setValuePx(width_adj, x1 - x0); + setValue(bmwidth_adj, width); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_x_value_changed() + +/// Called when area y0 value is changed. +void Export::areaYChange(Glib::RefPtr& adj) +{ + float y0, y1, ydpi, height; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + ydpi = getValue(ydpi_adj); + + height = floor ((y1 - y0) * ydpi / DPI_BASE + 0.5); + + if (height < SP_EXPORT_MIN_SIZE) { + //const gchar *key; + height = SP_EXPORT_MIN_SIZE; + //key = (const gchar *)g_object_get_data(G_OBJECT (adj), "key"); + if (adj == y1_adj) { + //if (!strcmp (key, "y0")) { + y1 = y0 + height * DPI_BASE / ydpi; + setValuePx(y1_adj, y1); + } else { + y0 = y1 - height * DPI_BASE / ydpi; + setValuePx(y0_adj, y0); + } + } + + setValuePx(height_adj, y1 - y0); + setValue(bmheight_adj, height); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_y_value_changed() + +/// Called when x1-x0 or area width is changed +void Export::onAreaWidthChange() +{ + if (update) { + return; + } + + update = true; + + float x0 = getValuePx(x0_adj); + float xdpi = getValue(xdpi_adj); + float width = getValuePx(width_adj); + float bmwidth = floor(width * xdpi / DPI_BASE + 0.5); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + + bmwidth = SP_EXPORT_MIN_SIZE; + width = bmwidth * DPI_BASE / xdpi; + setValuePx(width_adj, width); + } + + setValuePx(x1_adj, x0 + width); + setValue(bmwidth_adj, bmwidth); + + update = false; + + return; +} // end of sp_export_area_width_value_changed() + +/// Called when y1-y0 or area height is changed. +void Export::onAreaHeightChange() +{ + if (update) { + return; + } + + update = true; + + float y0 = getValuePx(y0_adj); + //float y1 = sp_export_value_get_px(y1_adj); + float ydpi = getValue(ydpi_adj); + float height = getValuePx(height_adj); + float bmheight = floor (height * ydpi / DPI_BASE + 0.5); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + height = bmheight * DPI_BASE / ydpi; + setValuePx(height_adj, height); + } + + setValuePx(y1_adj, y0 + height); + setValue(bmheight_adj, bmheight); + + update = false; + + return; +} // end of sp_export_area_height_value_changed() + +/** + * A function to set the ydpi. + * @param base The export dialog. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ +void Export::setImageY() +{ + float y0, y1, xdpi; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmheight_adj, (y1 - y0) * xdpi / DPI_BASE); + + return; +} // end of setImageY() + +/** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ +void Export::setImageX() +{ + float x0, x1, xdpi; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmwidth_adj, (x1 - x0) * xdpi / DPI_BASE); + + return; +} // end of setImageX() + +/// Called when pixel width is changed +void Export::onBitmapWidthChange () +{ + float x0, x1, bmwidth, xdpi; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + bmwidth = getValue(bmwidth_adj); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + setValue(bmwidth_adj, bmwidth); + } + + xdpi = bmwidth * DPI_BASE / (x1 - x0); + setValue(xdpi_adj, xdpi); + + setImageY (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/// Called when pixel height is changed +void Export::onBitmapHeightChange () +{ + float y0, y1, bmheight, xdpi; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + bmheight = getValue(bmheight_adj); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + setValue(bmheight_adj, bmheight); + } + + xdpi = bmheight * DPI_BASE / (y1 - y0); + setValue(xdpi_adj, xdpi); + + setImageX (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/** + * A function to adjust the bitmap width when the xdpi value changes. + * + * The first thing this function checks is to see if we are doing an + * update. If we are, this function just returns because there is another + * instance of it that will handle everything for us. If there is a + * units change, we also assume that everyone is being updated appropriately + * and there is nothing for us to do. + * + * If we're the highest level function, we set the update flag, and + * continue on our way. + * + * All of the values are grabbed using the \c sp_export_value_get functions + * (call to the _pt ones for x0 and x1 but just standard for xdpi). The + * xdpi value is saved in the preferences for the next time the dialog + * is opened. (does the selection dpi need to be set here?) + * + * A check is done to to ensure that we aren't outputting an invalid width, + * this is set by SP_EXPORT_MIN_SIZE. If that is the case the dpi is + * changed to make it valid. + * + * After all of this the bitmap width is changed. + * + * We also change the ydpi. This is a temporary hack as these can not + * currently be independent. This is likely to change in the future. + * + */ +void Export::onExportXdpiChange() +{ + float x0, x1, xdpi, bmwidth; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + // remember xdpi setting + prefs->setDouble("/dialogs/export/defaultxdpi/value", xdpi); + + bmwidth = (x1 - x0) * xdpi / DPI_BASE; + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + if (x1 != x0) + xdpi = bmwidth * DPI_BASE / (x1 - x0); + else + xdpi = DPI_BASE; + setValue(xdpi_adj, xdpi); + } + + setValue(bmwidth_adj, bmwidth); + + setImageY (); + + update = false; + + return; +} // end of sp_export_xdpi_value_changed() + + +/** + * A function to change the area that is used for the exported. + * bitmap. + * + * This function just calls \c sp_export_value_set_px for each of the + * parameters that is passed in. This allows for setting them all in + * one convenient area. + * + * Update is set to suspend all of the other test running while all the + * values are being set up. This allows for a performance increase, but + * it also means that the wrong type won't be detected with only some of + * the values set. After all the values are set everyone is told that + * there has been an update. + * + * @param x0 Horizontal upper left hand corner of the picture in points. + * @param y0 Vertical upper left hand corner of the picture in points. + * @param x1 Horizontal lower right hand corner of the picture in points. + * @param y1 Vertical lower right hand corner of the picture in points. + */ +void Export::setArea( double x0, double y0, double x1, double y1 ) +{ + update = true; + setValuePx(x1_adj, x1); + setValuePx(y1_adj, y1); + setValuePx(x0_adj, x0); + setValuePx(y0_adj, y0); + update = false; + + areaXChange (x1_adj); + areaYChange (y1_adj); + + return; +} + +/** + * Sets the value of an adjustment. + * + * @param adj The adjustment widget + * @param val What value to set it to. + */ +void Export::setValue(Glib::RefPtr& adj, double val ) +{ + if (adj) { + adj->set_value(val); + } +} + +/** + * A function to set a value using the units points. + * + * This function first gets the adjustment for the key that is passed + * in. It then figures out what units are currently being used in the + * dialog. After doing all of that, it then converts the incoming + *value and sets the adjustment. + * + * @param adj The adjustment widget + * @param val What the value should be in points. + */ +void Export::setValuePx(Glib::RefPtr& adj, double val) +{ + Unit const *unit = unit_selector.getUnit(); + + setValue(adj, Inkscape::Util::Quantity::convert(val, "px", unit)); + + return; +} + +/** + * Get the value of an adjustment in the export dialog. + * + * This function gets the adjustment from the data field in the export + * dialog. It then grabs the value from the adjustment. + * + * @param adj The adjustment widget + * + * @return The value in the specified adjustment. + */ +float Export::getValue(Glib::RefPtr& adj) +{ + if (!adj) { + g_message("sp_export_value_get : adj is NULL"); + return 0.0; + } + return adj->get_value(); +} + +/** + * Grabs a value in the export dialog and converts the unit + * to points. + * + * This function, at its most basic, is a call to \c sp_export_value_get + * to get the value of the adjustment. It then finds the units that + * are being used by looking at the "units" attribute of the export + * dialog. Using that it converts the returned value into points. + * + * @param adj The adjustment widget + * + * @return The value in the adjustment in points. + */ +float Export::getValuePx(Glib::RefPtr& adj) +{ + float value = getValue( adj); + Unit const *unit = unit_selector.getUnit(); + + return Inkscape::Util::Quantity::convert(value, unit, "px"); +} // end of sp_export_value_get_px() + +/** + * This function is called when the filename is changed by + * anyone. It resets the virgin bit. + * + * This function gets called when the text area is modified. It is + * looking for the case where the text area is modified from its + * original value. In that case it sets the "filename-modified" bit + * to TRUE. If the text dialog returns back to the original text, the + * bit gets reset. This should stop simple mistakes. + */ +void Export::onFilenameModified() +{ + if (original_name == filename_entry.get_text()) { + filename_modified = false; + } else { + filename_modified = true; + } + + return; +} // end sp_export_filename_modified + +} +} +} + +/* + 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 : diff --git a/src/ui/dialog/export.h b/src/ui/dialog/export.h new file mode 100644 index 0000000..9210ac5 --- /dev/null +++ b/src/ui/dialog/export.h @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * + * Copyright (C) 1999-2007 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_H +#define SP_EXPORT_H + +#include +#include +#include +#include + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** What type of button is being pressed */ +enum selection_type { + SELECTION_PAGE = 0, /**< Export the whole page */ + SELECTION_DRAWING, /**< Export everything drawn on the page */ + SELECTION_SELECTION, /**< Export everything that is selected */ + SELECTION_CUSTOM, /**< Allows the user to set the region exported */ + SELECTION_NUMBER_OF /**< A counter for the number of these guys */ +}; + +/** + * A dialog widget to export to various image formats such as bitmap and png. + * + * Creates a dialog window for exporting an image to a bitmap if one doesn't already exist and + * shows it to the user. If the dialog has already been created, it simply shows the window. + * + */ +class Export : public Widget::Panel { +public: + Export (); + ~Export () override; + + static Export &getInstance() { + return *new Export(); + } + +private: + + /** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ + void setImageX(); + + /** + * A function to set the ydpi. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ + void setImageY(); + bool bbox_equal(Geom::Rect const &one, Geom::Rect const &two); + void updateCheckbuttons (); + inline void findDefaultSelection(); + void detectSize(); + void setArea ( double x0, double y0, double x1, double y1); + /* + * Getter/setter style functions for the spinbuttons + */ + void setValue(Glib::RefPtr& adj, double val); + void setValuePx(Glib::RefPtr& adj, double val); + float getValue(Glib::RefPtr& adj); + float getValuePx(Glib::RefPtr& adj); + + /** + * Helper function to create, style and pack spinbuttons for the export dialog. + * + * Creates a new spin button for the export dialog. + * @param key The name of the spin button + * @param val A default value for the spin button + * @param min Minimum value for the spin button + * @param max Maximum value for the spin button + * @param step The step size for the spin button + * @param page Size of the page increment + * @param t Table to put the spin button in + * @param x X location in the table \c t to start with + * @param y Y location in the table \c t to start with + * @param ll Text to put on the left side of the spin button (optional) + * @param lr Text to put on the right side of the spin button (optional) + * @param digits Number of digits to display after the decimal + * @param sensitive Whether the spin button is sensitive or not + * @param cb Callback for when this spin button is changed (optional) + * + * No unit_selector is stored in the created spinbutton, relies on external unit management + */ + Glib::RefPtr createSpinbutton( gchar const *key, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ); + + /** + * One of the area select radio buttons was pressed + */ + void onAreaToggled(); + + /** + * Export button callback + */ + void onExport (); + + /** + * File Browse button callback + */ + void onBrowse (); + + /** + * Area X value changed callback + */ + void onAreaX0Change() { + areaXChange(x0_adj); + } ; + void onAreaX1Change() { + areaXChange(x1_adj); + } ; + void areaXChange(Glib::RefPtr& adj); + + /** + * Area Y value changed callback + */ + void onAreaY0Change() { + areaYChange(y0_adj); + } ; + void onAreaY1Change() { + areaYChange(y1_adj); + } ; + void areaYChange(Glib::RefPtr& adj); + + /** + * Unit changed callback + */ + void onUnitChanged(); + + /** + * Hide except selected callback + */ + void onHideExceptSelected (); + + /** + * Area width value changed callback + */ + void onAreaWidthChange (); + + /** + * Area height value changed callback + */ + void onAreaHeightChange (); + + /** + * Bitmap width value changed callback + */ + void onBitmapWidthChange (); + + /** + * Bitmap height value changed callback + */ + void onBitmapHeightChange (); + + /** + * Export xdpi value changed callback + */ + void onExportXdpiChange (); + + /** + * Batch export callback + */ + void onBatchClicked (); + + /** + * Inkscape selection change callback + */ + void onSelectionChanged (); + void onSelectionModified (guint flags); + + /** + * Filename modified callback + */ + void onFilenameModified (); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Creates progress dialog for batch exporting. + * + * @param progress_text Text to be shown in the progress bar + */ + Gtk::Dialog * create_progress_dialog (Glib::ustring progress_text); + + /** + * Callback to be used in for loop to update the progress bar. + * + * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress) + * @param dlg void pointer to the Gtk::Dialog progress dialog + */ + static unsigned int onProgressCallback(float value, void *dlg); + + /** + * Callback for pressing the cancel button. + */ + void onProgressCancel (); + + /** + * Callback invoked on closing the progress dialog. + */ + bool onProgressDelete (GdkEventAny *event); + + /** + * Handles state changes as exporting starts or stops. + */ + void setExporting(bool exporting, Glib::ustring const &text = ""); + + /* + * Utility filename and path functions + */ + void set_default_filename (); + Glib::ustring create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text); + Glib::ustring filename_add_extension (Glib::ustring filename, Glib::ustring extension); + Glib::ustring absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename); + + /* + * Currently selected export area type + */ + selection_type current_key; + /* + * Original name for the export object + */ + Glib::ustring original_name; + Glib::ustring doc_export_name; + /* + * Was the Original name modified + */ + bool filename_modified; + bool was_empty; + /* + * Flag to stop simultaneous updates + */ + bool update; + + /* Area selection radio buttons */ + Gtk::HBox togglebox; + Gtk::RadioButton *selectiontype_buttons[SELECTION_NUMBER_OF]; + + Gtk::VBox area_box; + Gtk::VBox singleexport_box; + + /* Custom size widgets */ + Glib::RefPtr x0_adj; + Glib::RefPtr x1_adj; + Glib::RefPtr y0_adj; + Glib::RefPtr y1_adj; + Glib::RefPtr width_adj; + Glib::RefPtr height_adj; + + /* Bitmap size widgets */ + Glib::RefPtr bmwidth_adj; + Glib::RefPtr bmheight_adj; + Glib::RefPtr xdpi_adj; + Glib::RefPtr ydpi_adj; + + Gtk::VBox size_box; + Gtk::Label* bm_label; + + Gtk::VBox file_box; + Gtk::Label *flabel; + Gtk::Entry filename_entry; + + /* Unit selector widgets */ + Gtk::HBox unitbox; + Inkscape::UI::Widget::UnitMenu unit_selector; + Gtk::Label units_label; + + /* Filename widgets */ + Gtk::HBox filename_box; + Gtk::Button browse_button; + Gtk::Label browse_label; + Gtk::Image browse_image; + + Gtk::HBox batch_box; + Gtk::CheckButton batch_export; + + Gtk::HBox hide_box; + Gtk::CheckButton hide_export; + + Gtk::CheckButton closeWhenDone; + + /* Advanced */ + Gtk::Expander expander; + Gtk::CheckButton interlacing; + Gtk::Label bitdepth_label; + Gtk::ComboBoxText bitdepth_cb; + Gtk::Label zlib_label; + Gtk::ComboBoxText zlib_compression; + Gtk::Label pHYs_label; + Glib::RefPtr pHYs_adj; + Gtk::SpinButton pHYs_sb; + Gtk::Label antialiasing_label; + Gtk::ComboBoxText antialiasing_cb; + + /* Export Button widgets */ + Gtk::HBox button_box; + Gtk::Button export_button; + + Gtk::ProgressBar _prog; + + Gtk::Dialog *prog_dlg; + bool interrupted; // indicates whether export needs to be interrupted (read: user pressed cancel in the progress dialog) + + Inkscape::Preferences *prefs; + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection unitChangedConn; + +}; + +} +} +} +#endif + +/* + 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 : diff --git a/src/ui/dialog/extension-editor.cpp b/src/ui/dialog/extension-editor.cpp new file mode 100644 index 0000000..1852010 --- /dev/null +++ b/src/ui/dialog/extension-editor.cpp @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Extension editor dialog. + */ +/* Authors: + * Bryce W. Harrington + * Ted Gould + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension-editor.h" +#include + +#include +#include + +#include "verbs.h" +#include "preferences.h" +#include "ui/interface.h" + +#include "extension/db.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Create a new ExtensionEditor dialog. + * + * This function creates a new extension editor dialog. The dialog + * consists of two basic areas. The left side is a tree widget, which + * is only used as a list. And the right side is a notebook of information + * about the selected extension. A handler is set up so that when + * a new extension is selected, the notebooks are changed appropriately. + */ +ExtensionEditor::ExtensionEditor() + : UI::Widget::Panel("/dialogs/extensioneditor", SP_VERB_DIALOG_EXTENSIONEDITOR) +{ + _notebook_info.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _notebook_params.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + //Main HBox + Gtk::HBox* hbox_list_page = Gtk::manage(new Gtk::HBox()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + _getContents()->add(*hbox_list_page); + + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _page_list.append_column("name",_page_list_columns._col_name); + Glib::RefPtr page_list_selection = _page_list.get_selection(); + page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &ExtensionEditor::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + + //Pages + Gtk::VBox* vbox_page = Gtk::manage(new Gtk::VBox()); + hbox_list_page->pack_start(*vbox_page, true, true, 0); + Gtk::Notebook * notebook = Gtk::manage(new Gtk::Notebook()); + notebook->append_page(_notebook_info, *Gtk::manage(new Gtk::Label(_("Information")))); + notebook->append_page(_notebook_params, *Gtk::manage(new Gtk::Label(_("Parameters")))); + vbox_page->pack_start(*notebook, true, true, 0); + + Inkscape::Extension::db.foreach(dbfunc, this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring defaultext = prefs->getString("/dialogs/extensioneditor/selected-extension"); + if (defaultext.empty()) defaultext = "org.inkscape.input.svg"; + this->setExtension(defaultext); + + show_all_children(); +} + +/** + * Destroys the extension editor dialog. + */ +ExtensionEditor::~ExtensionEditor() += default; + +void +ExtensionEditor::setExtension(Glib::ustring extension_id) { + _selection_search = extension_id; + _page_list_model->foreach_iter(sigc::mem_fun(*this, &ExtensionEditor::setExtensionIter)); + return; +} + +bool +ExtensionEditor::setExtensionIter(const Gtk::TreeModel::iterator &iter) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_page_list_columns._col_id] == _selection_search) { + _page_list.get_selection()->select(iter); + return true; + } + return false; +} + +/** + * Called every time a new extension is selected + * + * This function is set up to handle the signal for a changed extension + * from the tree view in the left pane. It figure out which extension + * is selected and updates the widgets to have data for that extension. + */ +void ExtensionEditor::on_pagelist_selection_changed() +{ + Glib::RefPtr selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + /* Get the row info */ + Gtk::TreeModel::Row row = *iter; + Glib::ustring id = row[_page_list_columns._col_id]; + Glib::ustring name = row[_page_list_columns._col_name]; + + /* Set the selection in the preferences */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/dialogs/extensioneditor/selected-extension", id); + + /* Adjust the dialog's title */ + gchar title[500]; + sp_ui_dialog_title_string (Inkscape::Verb::get(SP_VERB_DIALOG_EXTENSIONEDITOR), title); + Glib::ustring utitle(title); + // set_title(utitle + ": " + name); + + /* Clear the notbook pages */ + _notebook_info.remove(); + _notebook_params.remove(); + + Inkscape::Extension::Extension * ext = Inkscape::Extension::db.get(id.c_str()); + + /* Make sure we have all the widgets */ + Gtk::Widget * info = nullptr; + Gtk::Widget * params = nullptr; + + if (ext != nullptr) { + info = ext->get_info_widget(); + params = ext->get_params_widget(); + } + + /* Place them in the pages */ + if (info != nullptr) { + _notebook_info.add(*info); + } + if (params != nullptr) { + _notebook_params.add(*params); + } + + } + + return; +} + +/** + * A function to pass to the iterator in the Extensions Database. + * + * This function is a static function with the prototype required for + * the Extension Database's foreach function. It will get called for + * every extension in the database, and will then turn around and + * call the more object oriented function \c add_extension in the + * ExtensionEditor. + * + * @param in_plug The extension to evaluate. + * @param in_data A pointer to the Extension Editor class. + */ +void ExtensionEditor::dbfunc(Inkscape::Extension::Extension * in_plug, gpointer in_data) +{ + ExtensionEditor * ee = static_cast(in_data); + ee->add_extension(in_plug); + return; +} + +/** + * Adds an extension into the tree model. + * + * This function takes the data out of the extension and puts it + * into the tree model for the dialog. + * + * @param ext The extension to add. + * @return The iterator representing the location in the tree model. + */ +Gtk::TreeModel::iterator ExtensionEditor::add_extension(Inkscape::Extension::Extension * ext) +{ + Gtk::TreeModel::iterator iter; + + iter = _page_list_model->append(); + + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = ext->get_name(); + row[_page_list_columns._col_id] = ext->get_id(); + + return iter; +} + +} // 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 : diff --git a/src/ui/dialog/extension-editor.h b/src/ui/dialog/extension-editor.h new file mode 100644 index 0000000..403ee1f --- /dev/null +++ b/src/ui/dialog/extension-editor.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Extension editor dialog + */ +/* Authors: + * Bryce W. Harrington + * Ted Gould + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H +#define INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H + +#include "ui/widget/panel.h" + +#include +#include +#include + +#include "extension/extension.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ExtensionEditor : public UI::Widget::Panel { +public: + ExtensionEditor(); + ~ExtensionEditor() override; + + static ExtensionEditor &getInstance() { return *new ExtensionEditor(); } + +protected: + /** \brief The view of the list of extensions on the left of the dialog */ + Gtk::TreeView _page_list; + /** \brief The model for the list of extensions */ + Glib::RefPtr _page_list_model; + /** \brief The notebook page that contains information */ + Gtk::ScrolledWindow _notebook_info; + /** \brief The notebook page that holds all the parameters */ + Gtk::ScrolledWindow _notebook_params; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + /** \brief Creates the Page List model by adding all of the + members of the class as column records. */ + PageListModelColumns() { + Gtk::TreeModelColumnRecord::add(_col_name); + Gtk::TreeModelColumnRecord::add(_col_id); + } + /** \brief Name of the extension */ + Gtk::TreeModelColumn _col_name; + /** \brief ID of the extension */ + Gtk::TreeModelColumn _col_id; + }; + PageListModelColumns _page_list_columns; + +private: + /** \brief A 'global' variable to help search through and select + an item in the extension list */ + Glib::ustring _selection_search; + + ExtensionEditor(ExtensionEditor const &d) = delete; + ExtensionEditor& operator=(ExtensionEditor const &d) = delete; + + void on_pagelist_selection_changed(); + static void dbfunc (Inkscape::Extension::Extension * in_plug, gpointer in_data); + Gtk::TreeModel::iterator add_extension (Inkscape::Extension::Extension * ext); + bool setExtensionIter(const Gtk::TreeModel::iterator &iter); +public: + void setExtension(Glib::ustring extension_id); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H + +/* + 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 : diff --git a/src/ui/dialog/extensions.cpp b/src/ui/dialog/extensions.cpp new file mode 100644 index 0000000..f39c7d5 --- /dev/null +++ b/src/ui/dialog/extensions.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog with information about extensions. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extensions.h" +#include "extension/extension.h" +#include + +#include "extension/db.h" + + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +using Inkscape::Extension::Extension; + +ExtensionsPanel &ExtensionsPanel::getInstance() +{ + ExtensionsPanel &instance = *new ExtensionsPanel(); + + instance.rescan(); + + return instance; +} + + +ExtensionsPanel::ExtensionsPanel() : + _showAll(false) +{ + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + + _view.set_editable(false); + + scroller->add(_view); + add(*scroller); + + rescan(); + + show_all_children(); +} + +void ExtensionsPanel::set_full(bool full) +{ + if ( full != _showAll ) { + _showAll = full; + rescan(); + } +} + +void ExtensionsPanel::listCB( Inkscape::Extension::Extension * in_plug, gpointer in_data ) +{ + ExtensionsPanel * self = static_cast(in_data); + + const char* stateStr; + Extension::state_t state = in_plug->get_state(); + switch ( state ) { + case Extension::STATE_LOADED: + { + stateStr = "loaded"; + } + break; + case Extension::STATE_UNLOADED: + { + stateStr = "unloaded"; + } + break; + case Extension::STATE_DEACTIVATED: + { + stateStr = "deactivated"; + } + break; + default: + stateStr = "unknown"; + } + + if ( self->_showAll || in_plug->deactivated() ) { + gchar* line = g_strdup_printf( "%s %s\n \"%s\"", stateStr, in_plug->get_name(), in_plug->get_id() ); + + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), line ); + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), "\n" ); + g_free(line); + } + + return; +} + +void ExtensionsPanel::rescan() +{ + _view.get_buffer()->set_text("Extensions:\n"); +// g_message("/------------------"); + + Inkscape::Extension::db.foreach(listCB, (gpointer)this); + +// g_message("\\------------------"); +} + +} //namespace Dialogs +} //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 : diff --git a/src/ui/dialog/extensions.h b/src/ui/dialog/extensions.h new file mode 100644 index 0000000..16d2e91 --- /dev/null +++ b/src/ui/dialog/extensions.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A simple dialog with information about extensions + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_EXTENSIONS_H +#define SEEN_EXTENSIONS_H + +#include "ui/widget/panel.h" +#include + +namespace Inkscape { +namespace Extension { +class Extension; +} +} + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +/** + * A panel that displays information about extensions. + */ +class ExtensionsPanel : public Inkscape::UI::Widget::Panel +{ +public: + ExtensionsPanel(); + + static ExtensionsPanel &getInstance(); + + void set_full(bool full); + +private: + ExtensionsPanel(ExtensionsPanel const &) = delete; // no copy + ExtensionsPanel &operator=(ExtensionsPanel const &) = delete; // no assign + + static void listCB(Inkscape::Extension::Extension *in_plug, gpointer in_data); + + void rescan(); + + bool _showAll; + Gtk::TextView _view; +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif // SEEN_EXTENSIONS_H diff --git a/src/ui/dialog/filedialog.cpp b/src/ui/dialog/filedialog.cpp new file mode 100644 index 0000000..fdce498 --- /dev/null +++ b/src/ui/dialog/filedialog.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialog.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef _WIN32 +# include "filedialogimpl-win32.h" +# include "preferences.h" +#endif + +#include "filedialogimpl-gtkmm.h" + +#include "ui/dialog-events.h" +#include "extension/output.h" + +#include + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### U T I L I T Y +#########################################################################*/ + +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext) +{ + int strLen = str.length(); + int extLen = ext.length(); + if (extLen > strLen) + return false; + int strpos = strLen-1; + for (int extpos = extLen-1 ; extpos>=0 ; extpos--, strpos--) + { + Glib::ustring::value_type ch = str[strpos]; + if (ch != ext[extpos]) + { + if ( ((ch & 0xff80) != 0) || + static_cast( g_ascii_tolower( static_cast(0x07f & ch) ) ) != ext[extpos] ) + { + return false; + } + } + } + return true; +} + +bool isValidImageFile(const Glib::ustring &fileName) +{ + std::vectorformats = Gdk::Pixbuf::get_formats(); + for (auto format : formats) + { + std::vectorextensions = format.get_extensions(); + for (auto ext : extensions) + { + if (hasSuffix(fileName, ext)) + return true; + } + } + return false; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Public factory. Called by file.cpp, among others. + */ +FileOpenDialog *FileOpenDialog::create(Gtk::Window &parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title) +{ +#ifdef _WIN32 + FileOpenDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileOpenDialogImplWin32(parentWindow, path, fileTypes, title); + } else { + dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); + } +#else + FileOpenDialog *dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); +#endif + + return dialog; +} + +Glib::ustring FileOpenDialog::getFilename() +{ + return myFilename; +} + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Public factory method. Used in file.cpp + */ +FileSaveDialog *FileSaveDialog::create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) +{ +#ifdef _WIN32 + FileSaveDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileSaveDialogImplWin32(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } else { + dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } +#else + FileSaveDialog *dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); +#endif + return dialog; +} + +Glib::ustring FileSaveDialog::getFilename() +{ + return myFilename; +} + +Glib::ustring FileSaveDialog::getDocTitle() +{ + return myDocTitle; +} + +//void FileSaveDialog::change_path(const Glib::ustring& path) +//{ +// myFilename = path; +//} + +void FileSaveDialog::appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension) +{ + if (!outputExtension) + return; + + try { + bool appendExtension = true; + Glib::ustring utf8Name = Glib::filename_to_utf8( path ); + Glib::ustring::size_type pos = utf8Name.rfind('.'); + if ( pos != Glib::ustring::npos ) { + Glib::ustring trail = utf8Name.substr( pos ); + Glib::ustring foldedTrail = trail.casefold(); + if ( (trail == ".") + | (foldedTrail != Glib::ustring( outputExtension->get_extension() ).casefold() + && ( knownExtensions.find(foldedTrail) != knownExtensions.end() ) ) ) { + utf8Name = utf8Name.erase( pos ); + } else { + appendExtension = false; + } + } + + if (appendExtension) { + utf8Name = utf8Name + outputExtension->get_extension(); + myFilename = Glib::filename_from_utf8( utf8Name ); + } + } catch ( Glib::ConvertError& e ) { + // ignore + } +} + + +} //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 : diff --git a/src/ui/dialog/filedialog.h b/src/ui/dialog/filedialog.h new file mode 100644 index 0000000..8989171 --- /dev/null +++ b/src/ui/dialog/filedialog.h @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Virtual base definitions for native file dialogs + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Inkscape Guys + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008, Inkscape Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOG_H__ +#define __FILE_DIALOG_H__ + +#include +#include + +#include "extension/system.h" + +#include + +class SPDocument; + +namespace Inkscape { +namespace Extension { +class Extension; +class Output; +} +} + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/** + * Used for setting filters and options, and + * reading them back from user selections. + */ +enum FileDialogType { + SVG_TYPES, + IMPORT_TYPES, + EXPORT_TYPES, + EXE_TYPES, + SWATCH_TYPES, + CUSTOM_TYPE + }; + +/** + * Used for returning the type selected in a SaveAs + */ +enum FileDialogSelectionType { + SVG_NAMESPACE, + SVG_NAMESPACE_WITH_EXTENSIONS + }; + + +/** + * Return true if the string ends with the given suffix + */ +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext); + +/** + * Return true if the image is loadable by Gdk, else false + */ +bool isValidImageFile(const Glib::ustring &fileName); + +/** + * This class provides an implementation-independent API for + * file "Open" dialogs. Using a standard interface obviates the need + * for ugly #ifdefs in file open code + */ +class FileOpenDialog +{ +public: + + + /** + * Constructor .. do not call directly + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + FileOpenDialog() + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + static FileOpenDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileOpenDialog() = default;; + + /** + * Show an OpenFile file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() = 0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + Glib::ustring getFilename(); + + virtual std::vector getFilenames() = 0; + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + /** + * Filename that was given + */ + Glib::ustring myFilename; + +}; //FileOpenDialog + + + + + + +/** + * This class provides an implementation-independent API for + * file "Save" dialogs. + */ +class FileSaveDialog +{ +public: + + /** + * Constructor. Do not call directly . Use the factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + FileSaveDialog () + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + static FileSaveDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileSaveDialog() = default;; + + + /** + * Show an SaveAs file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() =0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + virtual void setSelectionType( Inkscape::Extension::Extension * key ) = 0; + + /** + * Get the file name chosen by the user. Valid after an [OK] + */ + Glib::ustring getFilename (); + + /** + * Get the document title chosen by the user. Valid after an [OK] + */ + Glib::ustring getDocTitle (); + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + + /** + * Filename that was given + */ + Glib::ustring myFilename; + + /** + * Doc Title that was given + */ + Glib::ustring myDocTitle; + + /** + * List of known file extensions. + */ + std::map knownExtensions; + + + void appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension); + +}; //FileSaveDialog + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* __FILE_DIALOG_H__ */ + +/* + 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 : diff --git a/src/ui/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp new file mode 100644 index 0000000..5874cac --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialogimpl.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "filedialogimpl-gtkmm.h" + +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "preferences.h" + +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "ui/dialog-events.h" +#include "ui/view/svg-view-widget.h" + +// Routines from file.cpp +#undef INK_DUMP_FILENAME_CONV + +#ifdef INK_DUMP_FILENAME_CONV +void dump_str(const gchar *str, const gchar *prefix); +void dump_ustr(const Glib::ustring &ustr); +#endif + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +//######################################################################## +//### U T I L I T Y +//######################################################################## + +void fileDialogExtensionToPattern(Glib::ustring &pattern, Glib::ustring &extension) +{ + for (unsigned int ch : extension) { + if (Glib::Unicode::isalpha(ch)) { + pattern += '['; + pattern += Glib::Unicode::toupper(ch); + pattern += Glib::Unicode::tolower(ch); + pattern += ']'; + } else { + pattern += ch; + } + } +} + + +void findEntryWidgets(Gtk::Container *parent, std::vector &result) +{ + if (!parent) { + return; + } + std::vector children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_ENTRY(wid)) + result.push_back(dynamic_cast(child)); + else if (GTK_IS_CONTAINER(wid)) + findEntryWidgets(dynamic_cast(child), result); + } +} + +void findExpanderWidgets(Gtk::Container *parent, std::vector &result) +{ + if (!parent) + return; + std::vector children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_EXPANDER(wid)) + result.push_back(dynamic_cast(child)); + else if (GTK_IS_CONTAINER(wid)) + findExpanderWidgets(dynamic_cast(child), result); + } +} + + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +void FileDialogBaseGtk::internalSetup() +{ + // Open executable file dialogs don't need the preview panel + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool enablePreview = prefs->getBool(preferenceBase + "/enable_preview", true); + bool enableSVGExport = prefs->getBool(preferenceBase + "/enable_svgexport", false); + + previewCheckbox.set_label(Glib::ustring(_("Enable preview"))); + previewCheckbox.set_active(enablePreview); + + previewCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_previewEnabledCB)); + + svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preference Dialog."))); + svgexportCheckbox.set_active(enableSVGExport); + + svgexportCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_svgexportEnabledCB)); + + // Catch selection-changed events, so we can adjust the text widget + signal_update_preview().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback)); + + //###### Add a preview widget + set_preview_widget(svgPreview); + set_preview_widget_active(enablePreview); + set_use_preview_label(false); + } +} + + +void FileDialogBaseGtk::cleanup(bool showConfirmed) +{ + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (showConfirmed) { + prefs->setBool(preferenceBase + "/enable_preview", previewCheckbox.get_active()); + } + } +} + + +void FileDialogBaseGtk::_previewEnabledCB() +{ + bool enabled = previewCheckbox.get_active(); + set_preview_widget_active(enabled); + if (enabled) { + _updatePreviewCallback(); + } else { + // Clears out any current preview image. + svgPreview.showNoPreview(); + } +} + +void FileDialogBaseGtk::_svgexportEnabledCB() +{ + bool enabled = svgexportCheckbox.get_active(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(preferenceBase + "/enable_svgexport", enabled); +} + + + +/** + * Callback for checking if the preview needs to be redrawn + */ +void FileDialogBaseGtk::_updatePreviewCallback() +{ + Glib::ustring fileName = get_preview_filename(); + bool enabled = previewCheckbox.get_active(); + + if (fileName.empty()) { + fileName = get_preview_uri(); + } + + if (enabled && !fileName.empty()) { + svgPreview.set(fileName, _dialogType); + } else { + svgPreview.showNoPreview(); + } +} + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplGtk::FileOpenDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_OPEN, fileTypes, "/dialogs/open") +{ + + + if (_dialogType == EXE_TYPES) { + /* One file at a time */ + set_select_multiple(false); + } else { + /* And also Multiple Files */ + set_select_multiple(true); + } + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (open, import, etc...)*/ + _dialogType = fileTypes; + + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') + udir.erase(len - 1); + if (_dialogType == EXE_TYPES) { + set_filename(udir.c_str()); + } else { + set_current_folder(udir.c_str()); + } + } + + if (_dialogType != EXE_TYPES) { + set_extra_widget(previewCheckbox); + } + + //###### Add the file types menu + createFilterMenu(); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Open"), Gtk::RESPONSE_OK)); + + //###### Allow easy access to our examples folder + if (Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_IS_DIR) && g_path_is_absolute(INKSCAPE_EXAMPLESDIR)) { + add_shortcut_folder(INKSCAPE_EXAMPLESDIR); + } +} + +/** + * Destructor + */ +FileOpenDialogImplGtk::~FileOpenDialogImplGtk() += default; + +void FileOpenDialogImplGtk::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_(name.c_str())); + allFilter->add_pattern(pattern); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); +} + +void FileOpenDialogImplGtk::createFilterMenu() +{ + if (_dialogType == CUSTOM_TYPE) { + return; + } + + if (_dialogType == EXE_TYPES) { + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + } else { + auto allInkscapeFilter = Gtk::FileFilter::create(); + allInkscapeFilter->set_name(_("All Inkscape Files")); + + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + + auto allImageFilter = Gtk::FileFilter::create(); + allImageFilter->set_name(_("All Images")); + + auto allVectorFilter = Gtk::FileFilter::create(); + allVectorFilter->set_name(_("All Vectors")); + + auto allBitmapFilter = Gtk::FileFilter::create(); + allBitmapFilter->set_name(_("All Bitmaps")); + extensionMap[Glib::ustring(_("All Inkscape Files"))] = nullptr; + add_filter(allInkscapeFilter); + + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + + extensionMap[Glib::ustring(_("All Images"))] = nullptr; + add_filter(allImageFilter); + + extensionMap[Glib::ustring(_("All Vectors"))] = nullptr; + add_filter(allVectorFilter); + + extensionMap[Glib::ustring(_("All Bitmaps"))] = nullptr; + add_filter(allBitmapFilter); + + // patterns added dynamically below + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + for (auto imod : extension_list) + { + // FIXME: would be nice to grey them out instead of not listing them + if (imod->deactivated()) + continue; + + Glib::ustring upattern("*"); + Glib::ustring extension = imod->get_extension(); + fileDialogExtensionToPattern(upattern, extension); + + Glib::ustring uname(imod->get_filetypename(true)); + + auto filter = Gtk::FileFilter::create(); + filter->set_name(uname); + filter->add_pattern(upattern); + add_filter(filter); + extensionMap[uname] = imod; + +// g_message("ext %s:%s '%s'\n", ioext->name, ioext->mimetype, upattern.c_str()); + allInkscapeFilter->add_pattern(upattern); + if (strncmp("image", imod->get_mimetype(), 5) == 0) + allImageFilter->add_pattern(upattern); + + // uncomment this to find out all mime types supported by Inkscape import/open + // g_print ("%s\n", imod->get_mimetype()); + + // I don't know of any other way to define "bitmap" formats other than by listing them + if (strncmp("image/png", imod->get_mimetype(), 9) == 0 || + strncmp("image/jpeg", imod->get_mimetype(), 10) == 0 || + strncmp("image/gif", imod->get_mimetype(), 9) == 0 || + strncmp("image/x-icon", imod->get_mimetype(), 12) == 0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22) == 0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18) == 0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/bmp", imod->get_mimetype(), 9) == 0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18) == 0 || + strncmp("image/tiff", imod->get_mimetype(), 10) == 0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/x-tga", imod->get_mimetype(), 11) == 0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11) == 0) + { + allBitmapFilter->add_pattern(upattern); + } else { + allVectorFilter->add_pattern(upattern); + } + } + } + return; +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileOpenDialogImplGtk::show() +{ + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + hide(); + + if (b == Gtk::RESPONSE_OK) { + // This is a hack, to avoid the warning messages that + // Gtk::FileChooser::get_filter() returns + // should be: Gtk::FileFilter *filter = get_filter(); + GtkFileChooser *gtkFileChooser = Gtk::FileChooser::gobj(); + GtkFileFilter *filter = gtk_file_chooser_get_filter(gtkFileChooser); + if (filter) { + // Get which extension was chosen, if any + extension = extensionMap[gtk_file_filter_get_name(filter)]; + } + myFilename = get_filename(); + + if (myFilename.empty()) { + myFilename = get_uri(); + } + + cleanup(true); + return true; + } else { + cleanup(false); + return false; + } +} + + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileOpenDialogImplGtk::getSelectionType() +{ + return extension; +} + + +/** + * Get the file name chosen by the user. Valid after an [OK] + */ +Glib::ustring FileOpenDialogImplGtk::getFilename() +{ + return myFilename; +} + + +/** + * To Get Multiple filenames selected at-once. + */ +std::vector FileOpenDialogImplGtk::getFilenames() +{ + auto result_tmp = get_filenames(); + + // Copy filenames to a vector of type Glib::ustring + std::vector result; + + for (auto it : result_tmp) + result.emplace_back(it); + + if (result.empty()) { + result = get_uris(); + } + + return result; +} + +Glib::ustring FileOpenDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Constructor + */ +FileSaveDialogImplGtk::FileSaveDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title, + const Glib::ustring & /*default_key*/, const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_SAVE, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "/dialogs/save_copy" + : "/dialogs/save_as") + , save_method(save_method) + , fromCB(false) +{ + FileSaveDialog::myDocTitle = docTitle; + + /* One file at a time */ + set_select_multiple(false); + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (save, export, etc...)*/ + _dialogType = fileTypes; + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if ((len != 0) && (udir[len - 1] == '\\')) { + udir.erase(len - 1); + } + myFilename = udir; + } + + //###### Add the file types menu + // createFilterMenu(); + + //###### Do we want the .xxx extension automatically added? + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + fileTypeCheckbox.set_label(Glib::ustring(_("Append filename extension automatically"))); + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_copy/append_extension", true)); + } else { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_as/append_extension", true)); + } + + if (_dialogType != CUSTOM_TYPE) + createFileTypeMenu(); + + fileTypeComboBox.set_size_request(200, 40); + fileTypeComboBox.signal_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileTypeChangedCallback)); + + + childBox.pack_start(checksBox); + childBox.pack_end(fileTypeComboBox); + checksBox.pack_start(fileTypeCheckbox); + checksBox.pack_start(previewCheckbox); + checksBox.pack_start(svgexportCheckbox); + + set_extra_widget(childBox); + + // Let's do some customization + fileNameEntry = nullptr; + Gtk::Container *cont = get_toplevel(); + std::vector entries; + findEntryWidgets(cont, entries); + // g_message("Found %d entry widgets\n", entries.size()); + if (!entries.empty()) { + // Catch when user hits [return] on the text field + fileNameEntry = entries[0]; + fileNameEntry->signal_activate().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback)); + } + signal_selection_changed().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged)); + + // Let's do more customization + std::vector expanders; + findExpanderWidgets(cont, expanders); + // g_message("Found %d expander widgets\n", expanders.size()); + if (!expanders.empty()) { + // Always show the file list + Gtk::Expander *expander = expanders[0]; + expander->set_expanded(true); + } + + // allow easy access to the user's own templates folder + using namespace Inkscape::IO::Resource; + char const *templates = Inkscape::IO::Resource::get_path(USER, TEMPLATES); + if (Inkscape::IO::file_test(templates, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(templates, G_FILE_TEST_IS_DIR) && g_path_is_absolute(templates)) { + add_shortcut_folder(templates); + } + + // if (extension == NULL) + // checkbox.set_sensitive(FALSE); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK)); + + show_all_children(); +} + +/** + * Destructor + */ +FileSaveDialogImplGtk::~FileSaveDialogImplGtk() += default; + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileNameEntryChangedCallback() +{ + if (!fileNameEntry) + return; + + Glib::ustring fileName = fileNameEntry->get_text(); + if (!Glib::get_charset()) // If we are not utf8 + fileName = Glib::filename_to_utf8(fileName); + + // g_message("User hit return. Text is '%s'\n", fileName.c_str()); + + if (!Glib::path_is_absolute(fileName)) { + // try appending to the current path + // not this way: fileName = get_current_folder() + "/" + fileName; + std::vector pathSegments; + pathSegments.emplace_back(get_current_folder()); + pathSegments.push_back(fileName); + fileName = Glib::build_filename(pathSegments); + } + + // g_message("path:'%s'\n", fileName.c_str()); + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) { + set_current_folder(fileName); + } else if (/*Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)*/ true) { + // dialog with either (1) select a regular file or (2) cd to dir + // simulate an 'OK' + set_filename(fileName); + response(Gtk::RESPONSE_OK); + } +} + + + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileTypeChangedCallback() +{ + int sel = fileTypeComboBox.get_active_row_number(); + if ((sel < 0) || (sel >= (int)fileTypes.size())) + return; + + FileType type = fileTypes[sel]; + // g_message("selected: %s\n", type.name.c_str()); + + extension = type.extension; + auto filter = Gtk::FileFilter::create(); + filter->add_pattern(type.pattern); + set_filter(filter); + + if (fromCB) { + //do not update if called from a name change + fromCB = false; + return; + } + + updateNameAndExtension(); +} + +void FileSaveDialogImplGtk::fileNameChanged() { + Glib::ustring name = get_filename(); + Glib::ustring::size_type pos = name.rfind('.'); + if ( pos == Glib::ustring::npos ) return; + Glib::ustring ext = name.substr( pos ).casefold(); + if (extension && Glib::ustring(dynamic_cast(extension)->get_extension()).casefold() == ext ) return; + if (knownExtensions.find(ext) == knownExtensions.end()) return; + fromCB = true; + fileTypeComboBox.set_active_text(knownExtensions[ext]->get_filetypename(true)); +} + +void FileSaveDialogImplGtk::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + //#Let user choose + FileType guessType; + guessType.name = name; + guessType.pattern = pattern; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + +void FileSaveDialogImplGtk::createFileTypeMenu() +{ + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + knownExtensions.clear(); + + for (auto omod : extension_list) { + // FIXME: would be nice to grey them out instead of not listing them + if (omod->deactivated()) + continue; + + FileType type; + type.name = omod->get_filetypename(true); + type.pattern = "*"; + Glib::ustring extension = omod->get_extension(); + knownExtensions.insert(std::pair(extension.casefold(), omod)); + fileDialogExtensionToPattern(type.pattern, extension); + type.extension = omod; + fileTypeComboBox.append(type.name); + fileTypes.push_back(type); + } + + //#Let user choose + FileType guessType; + guessType.name = _("Guess from extension"); + guessType.pattern = "*"; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + + + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileSaveDialogImplGtk::show() +{ + change_path(myFilename); + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + set_preview_widget_active(false); + hide(); + + if (b == Gtk::RESPONSE_OK) { + updateNameAndExtension(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Store changes of the "Append filename automatically" checkbox back to preferences. + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + prefs->setBool("/dialogs/save_copy/append_extension", fileTypeCheckbox.get_active()); + } else { + prefs->setBool("/dialogs/save_as/append_extension", fileTypeCheckbox.get_active()); + } + + Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method); + + cleanup(true); + + return true; + } else { + cleanup(false); + return false; + } +} + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileSaveDialogImplGtk::getSelectionType() +{ + return extension; +} + +void FileSaveDialogImplGtk::setSelectionType(Inkscape::Extension::Extension *key) +{ + // If no pointer to extension is passed in, look up based on filename extension. + if (!key) { + // Not quite UTF-8 here. + gchar *filenameLower = g_ascii_strdown(myFilename.c_str(), -1); + for (int i = 0; !key && (i < (int)fileTypes.size()); i++) { + Inkscape::Extension::Output *ext = dynamic_cast(fileTypes[i].extension); + if (ext && ext->get_extension()) { + gchar *extensionLower = g_ascii_strdown(ext->get_extension(), -1); + if (g_str_has_suffix(filenameLower, extensionLower)) { + key = fileTypes[i].extension; + } + g_free(extensionLower); + } + } + g_free(filenameLower); + } + + // Ensure the proper entry in the combo box is selected. + if (key) { + extension = key; + gchar const *extensionID = extension->get_id(); + if (extensionID) { + for (int i = 0; i < (int)fileTypes.size(); i++) { + Inkscape::Extension::Extension *ext = fileTypes[i].extension; + if (ext) { + gchar const *id = ext->get_id(); + if (id && (strcmp(extensionID, id) == 0)) { + int oldSel = fileTypeComboBox.get_active_row_number(); + if (i != oldSel) { + fileTypeComboBox.set_active(i); + } + break; + } + } + } + } + } +} + +Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + +/*void +FileSaveDialogImplGtk::change_title(const Glib::ustring& title) +{ + set_title(title); +}*/ + +/** + * Change the default save path location. + */ +void FileSaveDialogImplGtk::change_path(const Glib::ustring &path) +{ + myFilename = path; + + if (Glib::file_test(myFilename, Glib::FILE_TEST_IS_DIR)) { + // fprintf(stderr,"set_current_folder(%s)\n",myFilename.c_str()); + set_current_folder(myFilename); + } else { + // fprintf(stderr,"set_filename(%s)\n",myFilename.c_str()); + if (Glib::file_test(myFilename, Glib::FILE_TEST_EXISTS)) { + set_filename(myFilename); + } else { + std::string dirName = Glib::path_get_dirname(myFilename); + if (dirName != get_current_folder()) { + set_current_folder(dirName); + } + } + Glib::ustring basename = Glib::path_get_basename(myFilename); + // fprintf(stderr,"set_current_name(%s)\n",basename.c_str()); + try + { + set_current_name(Glib::filename_to_utf8(basename)); + } + catch (Glib::ConvertError &e) + { + g_warning("Error converting save filename to UTF-8."); + // try a fallback. + set_current_name(basename); + } + } +} + +void FileSaveDialogImplGtk::updateNameAndExtension() +{ + // Pick up any changes the user has typed in. + Glib::ustring tmp = get_filename(); + + if (tmp.empty()) { + tmp = get_uri(); + } + + if (!tmp.empty()) { + myFilename = tmp; + } + + Inkscape::Extension::Output *newOut = extension ? dynamic_cast(extension) : nullptr; + if (fileTypeCheckbox.get_active() && newOut) { + // Append the file extension if it's not already present and display it in the file name entry field + appendExtension(myFilename, newOut); + change_path(myFilename); + } +} + + +} // 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 : diff --git a/src/ui/dialog/filedialogimpl-gtkmm.h b/src/ui/dialog/filedialogimpl-gtkmm.h new file mode 100644 index 0000000..430f6b6 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.h @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h + */ +/* Authors: + * Bob Jamison + * Johan Engelen + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2008 Authors + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOGIMPL_H__ +#define __FILE_DIALOGIMPL_H__ + +//Gtk includes +#include +#include + +#include "filedialog.h" +#include "svg-preview.h" + +namespace Gtk { +class CheckButton; +class ComboBoxText; +class Expander; +} + +namespace Inkscape { + class URI; + +namespace UI { + +namespace View { + class SVGViewWidget; +} + +namespace Dialog { + +/*######################################################################### +### Utility +#########################################################################*/ +void +fileDialogExtensionToPattern(Glib::ustring &pattern, + Glib::ustring &extension); + +void +findEntryWidgets(Gtk::Container *parent, + std::vector &result); + +void +findExpanderWidgets(Gtk::Container *parent, + std::vector &result); + +class FileType +{ + public: + FileType(): name(), pattern(),extension(nullptr) {} + ~FileType() = default; + Glib::ustring name; + Glib::ustring pattern; + Inkscape::Extension::Extension *extension; +}; + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/** + * This class is the base implementation for the others. This + * reduces redundancies and bugs. + */ +class FileDialogBaseGtk : public Gtk::FileChooserDialog +{ +public: + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const Glib::ustring &title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const char *title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + ~FileDialogBaseGtk() override + = default; + +protected: + void cleanup( bool showConfirmed ); + + Glib::ustring const preferenceBase; + /** + * What type of 'open' are we? (open, import, place, etc) + */ + FileDialogType _dialogType; + + /** + * Our svg preview widget + */ + SVGPreview svgPreview; + + /** + * Child widgets + */ + Gtk::CheckButton previewCheckbox; + Gtk::CheckButton svgexportCheckbox; + +private: + void internalSetup(); + + /** + * Callback for user changing preview checkbox + */ + void _previewEnabledCB(); + + /** + * Callback for seeing if the preview needs to be drawn + */ + void _updatePreviewCallback(); + + /** + * Callback to for SVG 2 to SVG 1.1 export. + */ + void _svgexportEnabledCB(); +}; + + + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Our implementation class for the FileOpenDialog interface.. + */ +class FileOpenDialogImplGtk : public FileOpenDialog, public FileDialogBaseGtk +{ +public: + + FileOpenDialogImplGtk(Gtk::Window& parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title); + + ~FileOpenDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + + Glib::ustring getFilename(); + + std::vector getFilenames() override; + + Glib::ustring getCurrentDirectory() override; + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering patter (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + void addFilterMenu(Glib::ustring name, Glib::ustring pattern) override; + +private: + + /** + * Create a filter menu for this type of dialog + */ + void createFilterMenu(); + + + /** + * Filter name->extension lookup + */ + std::map extensionMap; + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + +}; + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Our implementation of the FileSaveDialog interface. + */ +class FileSaveDialogImplGtk : public FileSaveDialog, public FileDialogBaseGtk +{ + +public: + FileSaveDialogImplGtk(Gtk::Window &parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title, + const Glib::ustring &default_key, + const gchar* docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + ~FileSaveDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + void setSelectionType( Inkscape::Extension::Extension * key ) override; + + Glib::ustring getCurrentDirectory() override; + void addFileType(Glib::ustring name, Glib::ustring pattern) override; + +private: + //void change_title(const Glib::ustring& title); + void change_path(const Glib::ustring& path); + void updateNameAndExtension(); + + /** + * The file save method (essentially whether the dialog was invoked by "Save as ..." or "Save a + * copy ..."), which is used to determine file extensions and save paths. + */ + Inkscape::Extension::FileSaveMethod save_method; + + /** + * Fix to allow the user to type the file name + */ + Gtk::Entry *fileNameEntry; + + + /** + * Allow the specification of the output file type + */ + Gtk::ComboBoxText fileTypeComboBox; + + + /** + * Data mirror of the combo box + */ + std::vector fileTypes; + + //# Child widgets + Gtk::HBox childBox; + Gtk::VBox checksBox; + + Gtk::CheckButton fileTypeCheckbox; + + /** + * Callback for user input into fileNameEntry + */ + void fileTypeChangedCallback(); + + /** + * Create a filter menu for this type of dialog + */ + void createFileTypeMenu(); + + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + + /** + * Callback for user input into fileNameEntry + */ + void fileNameEntryChangedCallback(); + void fileNameChanged(); + bool fromCB; +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif /*__FILE_DIALOGIMPL_H__*/ + +/* + 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 : diff --git a/src/ui/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp new file mode 100644 index 0000000..8c37176 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.cpp @@ -0,0 +1,1937 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of native file dialogs for Win32. + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef _WIN32 + +#include "filedialogimpl-win32.h" +// General includes +#include +#include +#include +#include +#include +#include +#include + +//Inkscape includes +#include "display/cairo-utils.h" +#include "document.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "filedialog.h" +#include "helper/pixbuf-ops.h" +#include "preferences.h" +#include "util/units.h" + + +using namespace Glib; +using namespace Cairo; +using namespace Gdk::Cairo; + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +const int PREVIEW_WIDENING = 150; +const int WINDOW_WIDTH_MINIMUM = 32; +const int WINDOW_WIDTH_FALLBACK = 450; +const int WINDOW_HEIGHT_MINIMUM = 32; +const int WINDOW_HEIGHT_FALLBACK = 360; +const char PreviewWindowClassName[] = "PreviewWnd"; +const unsigned long MaxPreviewFileSize = 10240; // kB + +#define IDC_SHOW_PREVIEW 1000 + +struct Filter +{ + gunichar2* name; + glong name_length; + gunichar2* filter; + glong filter_length; + Inkscape::Extension::Extension* mod; +}; + +ustring utf16_to_ustring(const wchar_t *utf16string, int utf16length = -1) +{ + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)utf16string, + utf16length, NULL, NULL, NULL); + ustring result(utf8string); + g_free(utf8string); + + return result; +} + +namespace { + +int sanitizeWindowSizeParam( int size, int delta, int minimum, int fallback ) +{ + int result = size; + if ( size < minimum ) { + g_warning( "Window size %d is less than cutoff.", size ); + result = fallback - delta; + } + result += delta; + return result; +} + +} // namespace + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +FileDialogBaseWin32::FileDialogBaseWin32(Gtk::Window &parent, + const Glib::ustring &dir, const gchar *title, + FileDialogType type, gchar const* /*preferenceBase*/) : + dialogType(type), + parent(parent), + _current_directory(dir) +{ + _main_loop = NULL; + + _filter_index = 1; + _filter_count = 0; + + _title = (wchar_t*)g_utf8_to_utf16(title, -1, NULL, NULL, NULL); + g_assert(_title != NULL); + + Glib::RefPtr parentWindow = parent.get_window(); + g_assert(parentWindow->gobj() != NULL); + _ownerHwnd = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); +} + +FileDialogBaseWin32::~FileDialogBaseWin32() +{ + g_free(_title); +} + +Inkscape::Extension::Extension *FileDialogBaseWin32::getSelectionType() +{ + return _extension; +} + +Glib::ustring FileDialogBaseWin32::getCurrentDirectory() +{ + return _current_directory; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +bool FileOpenDialogImplWin32::_show_preview = true; + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplWin32::FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const gchar *title) : + FileDialogBaseWin32(parent, dir, title, fileTypes, "dialogs.open") +{ + // Initialize to Autodetect + _extension = NULL; + + // Set our dialog type (open, import, etc...) + dialogType = fileTypes; + + _show_preview_button_bitmap = NULL; + _preview_wnd = NULL; + _file_dialog_wnd = NULL; + _base_window_proc = NULL; + + _preview_file_size = 0; + _preview_bitmap = NULL; + _preview_file_icon = NULL; + _preview_document_width = 0; + _preview_document_height = 0; + _preview_image_width = 0; + _preview_image_height = 0; + _preview_emf_image = false; + + _mutex = NULL; + + if (dialogType != CUSTOM_TYPE) + createFilterMenu(); +} + + +/** + * Destructor + */ +FileOpenDialogImplWin32::~FileOpenDialogImplWin32() +{ + if(_filter != NULL) + delete[] _filter; + if(_extension_map != NULL) + delete[] _extension_map; +} + +void FileOpenDialogImplWin32::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + std::list filter_list; + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list +} + +void FileOpenDialogImplWin32::createFilterMenu() +{ + std::list filter_list; + + int extension_index = 0; + int filter_length = 1; + + if (dialogType == CUSTOM_TYPE) { + return; + } + + if (dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_preview = prefs->getBool("/dialogs/open/enable_preview", true); + + // Compose the filter string + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + ustring all_inkscape_files_filter, all_image_files_filter, all_vectors_filter, all_bitmaps_filter; + Filter all_files, all_inkscape_files, all_image_files, all_vectors, all_bitmaps; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_inkscape_files_filter_name = _("All Inkscape Files"); + const gchar *all_image_files_filter_name = _("All Images"); + const gchar *all_vectors_filter_name = _("All Vectors"); + const gchar *all_bitmaps_filter_name = _("All Bitmaps"); + + // Calculate the amount of memory required + int filter_count = 5; // 5 - one for each filter type + + for (Inkscape::Extension::DB::InputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Filter filter; + + Inkscape::Extension::Input *imod = *current_item; + if (imod->deactivated()) continue; + + // Type + filter.name = g_utf8_to_utf16(imod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + // Extension + const gchar *file_extension_name = imod->get_extension(); + filter.filter = g_utf8_to_utf16(file_extension_name, -1, NULL, &filter.filter_length, NULL); + + filter.mod = imod; + filter_list.push_back(filter); + + filter_length += filter.name_length + filter.filter_length + 3; // Add 3 for two \0s and a * + + // Add to the "All Inkscape Files" Entry + if(all_inkscape_files_filter.length() > 0) + all_inkscape_files_filter += ";*"; + all_inkscape_files_filter += file_extension_name; + if( strncmp("image", imod->get_mimetype(), 5) == 0) + { + // Add to the "All Image Files" Entry + if(all_image_files_filter.length() > 0) + all_image_files_filter += ";*"; + all_image_files_filter += file_extension_name; + } + + // I don't know of any other way to define "bitmap" formats other than by listing them + // if you change it here, do the same change in filedialogimpl-gtkmm + if ( + strncmp("image/png", imod->get_mimetype(), 9)==0 || + strncmp("image/jpeg", imod->get_mimetype(), 10)==0 || + strncmp("image/gif", imod->get_mimetype(), 9)==0 || + strncmp("image/x-icon", imod->get_mimetype(), 12)==0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22)==0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18)==0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15)==0 || + strncmp("image/bmp", imod->get_mimetype(), 9)==0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18)==0 || + strncmp("image/tiff", imod->get_mimetype(), 10)==0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15)==0 || + strncmp("image/x-tga", imod->get_mimetype(), 11)==0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11)==0 + ) { + if(all_bitmaps_filter.length() > 0) + all_bitmaps_filter += ";*"; + all_bitmaps_filter += file_extension_name; + } else { + if(all_vectors_filter.length() > 0) + all_vectors_filter += ";*"; + all_vectors_filter += file_extension_name; + } + + filter_count++; + } + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter bitmap files + all_bitmaps.name = g_utf8_to_utf16(all_bitmaps_filter_name, + -1, NULL, &all_bitmaps.name_length, NULL); + all_bitmaps.filter = g_utf8_to_utf16(all_bitmaps_filter.data(), + -1, NULL, &all_bitmaps.filter_length, NULL); + all_bitmaps.mod = NULL; + filter_list.push_front(all_bitmaps); + + // Filter vector files + all_vectors.name = g_utf8_to_utf16(all_vectors_filter_name, + -1, NULL, &all_vectors.name_length, NULL); + all_vectors.filter = g_utf8_to_utf16(all_vectors_filter.data(), + -1, NULL, &all_vectors.filter_length, NULL); + all_vectors.mod = NULL; + filter_list.push_front(all_vectors); + + // Filter Image Files + all_image_files.name = g_utf8_to_utf16(all_image_files_filter_name, + -1, NULL, &all_image_files.name_length, NULL); + all_image_files.filter = g_utf8_to_utf16(all_image_files_filter.data(), + -1, NULL, &all_image_files.filter_length, NULL); + all_image_files.mod = NULL; + filter_list.push_front(all_image_files); + + // Filter Inkscape Files + all_inkscape_files.name = g_utf8_to_utf16(all_inkscape_files_filter_name, + -1, NULL, &all_inkscape_files.name_length, NULL); + all_inkscape_files.filter = g_utf8_to_utf16(all_inkscape_files_filter.data(), + -1, NULL, &all_inkscape_files.filter_length, NULL); + all_inkscape_files.mod = NULL; + filter_list.push_front(all_inkscape_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_inkscape_files.filter_length + + all_inkscape_files.name_length + 3 + + all_image_files.filter_length + + all_image_files.name_length + 3 + + all_vectors.filter_length + + all_vectors.name_length + 3 + + all_bitmaps.filter_length + + all_bitmaps.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } else { + // Executables only + ustring all_exe_files_filter = "*.exe;*.bat;*.com"; + Filter all_exe_files, all_files; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_exe_files_filter_name = _("All Executable Files"); + + // Calculate the amount of memory required + int filter_count = 2; // 2 - All Files and All Executable Files + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter.data(), + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_exe_files.filter_length + + all_exe_files.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 2; // Select the 2nd filter in the list - 2 is NOT the 3rd +} + +void FileOpenDialogImplWin32::GetOpenFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_mutex != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + memset(&ofn, 0, sizeof(ofn)); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_HIDEREADONLY | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetOpenFileName_hookproc; + ofn.lCustData = (LPARAM)this; + + _result = GetOpenFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + _mutex->lock(); + _finished = true; + _mutex->unlock(); +} + +void FileOpenDialogImplWin32::register_preview_wnd_class() +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + const WNDCLASSA PreviewWndClass = + { + CS_HREDRAW | CS_VREDRAW, + preview_wnd_proc, + 0, + 0, + hInstance, + NULL, + LoadCursor(hInstance, IDC_ARROW), + (HBRUSH)(COLOR_BTNFACE + 1), + NULL, + PreviewWindowClassName + }; + + RegisterClassA(&PreviewWndClass); +} + +UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast(ofn->lCustData); + + // Make the window a bit wider + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + + // Don't show the preview when opening executable files + if ( pImpl->dialogType == EXE_TYPES) { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left, + rcRect.bottom - rcRect.top, + FALSE); + } else { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left + PREVIEW_WIDENING, + rcRect.bottom - rcRect.top, + FALSE); + } + + // Subclass the parent + pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC); + SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast(file_dialog_subclass_proc)); + + if ( pImpl->dialogType != EXE_TYPES) { + // Add a button to the toolbar + pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL); + + pImpl->_show_preview_button_bitmap = LoadBitmap( + hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW)); + TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast(pImpl->_show_preview_button_bitmap)}; + const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd, + TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap); + + + TBBUTTON tbButton; + memset(&tbButton, 0, sizeof(TBBUTTON)); + tbButton.iBitmap = iBitmapIndex; + tbButton.idCommand = IDC_SHOW_PREVIEW; + tbButton.fsState = (pImpl->_show_preview ? TBSTATE_CHECKED : 0) + | TBSTATE_ENABLED; + tbButton.fsStyle = TBSTYLE_CHECK; + tbButton.iString = (INT_PTR)_("Show Preview"); + SendMessage(pImpl->_toolbar_wnd, TB_ADDBUTTONS, 1, (LPARAM)&tbButton); + + // Create preview pane + register_preview_wnd_class(); + } + + pImpl->_mutex->lock(); + + pImpl->_file_dialog_wnd = hParentWnd; + + if ( pImpl->dialogType != EXE_TYPES) { + pImpl->_preview_wnd = + CreateWindowA(PreviewWindowClassName, "", + WS_CHILD | WS_VISIBLE, + 0, 0, 100, 100, hParentWnd, NULL, hInstance, NULL); + SetWindowLongPtr(pImpl->_preview_wnd, GWLP_USERDATA, ofn->lCustData); + } + + pImpl->_mutex->unlock(); + + pImpl->layout_dialog(); + } + break; + + case WM_NOTIFY: + { + + OFNOTIFY *pOFNotify = reinterpret_cast(lParam); + switch(pOFNotify->hdr.code) + { + case CDN_SELCHANGE: + { + if(pImpl != NULL) + { + // Get the file name + pImpl->_mutex->lock(); + + SendMessage(pOFNotify->hdr.hwndFrom, CDM_GETFILEPATH, + sizeof(pImpl->_path_string) / sizeof(wchar_t), + (LPARAM)pImpl->_path_string); + + pImpl->_file_selected = true; + + pImpl->_mutex->unlock(); + } + } + break; + } + } + break; + + case WM_CLOSE: + pImpl->_mutex->lock(); + pImpl->_preview_file_size = 0; + + pImpl->_file_dialog_wnd = NULL; + DestroyWindow(pImpl->_preview_wnd); + pImpl->_preview_wnd = NULL; + DeleteObject(pImpl->_show_preview_button_bitmap); + pImpl->_show_preview_button_bitmap = NULL; + pImpl->_mutex->unlock(); + + break; + } + + // Use default dialog behaviour + return 0; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = CallWindowProc(pImpl->_base_window_proc, hwnd, uMsg, wParam, lParam); + + switch(uMsg) + { + case WM_SHOWWINDOW: + if(wParam != 0) + pImpl->layout_dialog(); + break; + + case WM_SIZE: + pImpl->layout_dialog(); + break; + + case WM_COMMAND: + if(wParam == IDC_SHOW_PREVIEW) + { + const bool enable = SendMessage(pImpl->_toolbar_wnd, + TB_ISBUTTONCHECKED, IDC_SHOW_PREVIEW, 0) != 0; + pImpl->enable_preview(enable); + } + break; + } + + return lResult; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + const int CaptionPadding = 4; + const int IconSize = 32; + + FileOpenDialogImplWin32 *pImpl = reinterpret_cast + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = 0; + + switch(uMsg) + { + case WM_ERASEBKGND: + // Do nothing to erase the background + // - otherwise there'll be flicker + lResult = 1; + break; + + case WM_PAINT: + { + // Get the client rect + RECT rcClient; + GetClientRect(hwnd, &rcClient); + + // Prepare to paint + PAINTSTRUCT paint_struct; + HDC dc = BeginPaint(hwnd, &paint_struct); + + HFONT hCaptionFont = reinterpret_cast(SendMessage(GetParent(hwnd), + WM_GETFONT, 0, 0)); + HFONT hOldFont = static_cast(SelectObject(dc, hCaptionFont)); + SetBkMode(dc, TRANSPARENT); + + pImpl->_mutex->lock(); + + if(pImpl->_path_string[0] == 0) + { + WCHAR* noFileText=(WCHAR*)g_utf8_to_utf16(_("No file selected"), + -1, NULL, NULL, NULL); + FillRect(dc, &rcClient, reinterpret_cast(COLOR_3DFACE + 1)); + DrawTextW(dc, noFileText, -1, &rcClient, + DT_CENTER | DT_VCENTER | DT_NOPREFIX); + g_free(noFileText); + } + else if(pImpl->_preview_bitmap != NULL) + { + BITMAP bitmap; + GetObject(pImpl->_preview_bitmap, sizeof(bitmap), &bitmap); + const int destX = (rcClient.right - bitmap.bmWidth) / 2; + + // Render the image + HDC hSrcDC = CreateCompatibleDC(dc); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hSrcDC, pImpl->_preview_bitmap); + + BitBlt(dc, destX, 0, bitmap.bmWidth, bitmap.bmHeight, + hSrcDC, 0, 0, SRCCOPY); + + SelectObject(hSrcDC, hOldBitmap); + DeleteDC(hSrcDC); + + // Fill in the background area + HRGN hEraseRgn = CreateRectRgn(rcClient.left, rcClient.top, + rcClient.right, rcClient.bottom); + HRGN hImageRgn = CreateRectRgn(destX, 0, + destX + bitmap.bmWidth, bitmap.bmHeight); + CombineRgn(hEraseRgn, hEraseRgn, hImageRgn, RGN_DIFF); + + FillRgn(dc, hEraseRgn, GetSysColorBrush(COLOR_3DFACE)); + + DeleteObject(hImageRgn); + DeleteObject(hEraseRgn); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + bitmap.bmHeight + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = pImpl->format_caption( + szCaption, sizeof(szCaption) / sizeof(WCHAR)); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else if(pImpl->_preview_file_icon != NULL) + { + FillRect(dc, &rcClient, reinterpret_cast(COLOR_3DFACE + 1)); + + // Draw the files icon + const int destX = (rcClient.right - IconSize) / 2; + DrawIconEx(dc, destX, 0, pImpl->_preview_file_icon, + IconSize, IconSize, 0, NULL, + DI_NORMAL | DI_COMPAT); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + IconSize + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szFileName[_MAX_FNAME], szCaption[_MAX_FNAME + 32]; + _wsplitpath(pImpl->_path_string, NULL, NULL, szFileName, NULL); + + const int iLength = snwprintf(szCaption, + sizeof(szCaption), L"%ls\n%d kB", + szFileName, pImpl->_preview_file_size); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else + { + // Can't show anything! + FillRect(dc, &rcClient, reinterpret_cast(COLOR_3DFACE + 1)); + } + + pImpl->_mutex->unlock(); + + // Finish painting + SelectObject(dc, hOldFont); + EndPaint(hwnd, &paint_struct); + } + + break; + + case WM_DESTROY: + pImpl->free_preview(); + break; + + default: + lResult = DefWindowProc(hwnd, uMsg, wParam, lParam); + break; + } + + return lResult; +} + +void FileOpenDialogImplWin32::enable_preview(bool enable) +{ + if (_show_preview != enable) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/open/enable_preview", enable); + } + _show_preview = enable; + + // Relayout the dialog + ShowWindow(_preview_wnd, enable ? SW_SHOW : SW_HIDE); + layout_dialog(); + + // Load or unload the preview + if(enable) + { + _mutex->lock(); + _file_selected = true; + _mutex->unlock(); + } + else free_preview(); +} + +void FileOpenDialogImplWin32::layout_dialog() +{ + union RECTPOINTS + { + RECT r; + POINT p[2]; + }; + + const float MaxExtentScale = 2.0f / 3.0f; + + RECT rcClient; + GetClientRect(_file_dialog_wnd, &rcClient); + + // Re-layout the dialog + HWND hFileListWnd = GetDlgItem(_file_dialog_wnd, lst2); + HWND hFolderComboWnd = GetDlgItem(_file_dialog_wnd, cmb2); + + + RECT rcFolderComboRect; + RECTPOINTS rcFileList; + GetWindowRect(hFileListWnd, &rcFileList.r); + GetWindowRect(hFolderComboWnd, &rcFolderComboRect); + const int iPadding = rcFileList.r.top - rcFolderComboRect.bottom; + MapWindowPoints(NULL, _file_dialog_wnd, rcFileList.p, 2); + + RECT rcPreview; + RECT rcBody = {rcFileList.r.left, rcFileList.r.top, + rcClient.right - iPadding, rcFileList.r.bottom}; + rcFileList.r.right = rcBody.right; + + if(_show_preview && dialogType != EXE_TYPES) + { + rcPreview.top = rcBody.top; + rcPreview.left = rcClient.right - (rcBody.bottom - rcBody.top); + const int iMaxExtent = (int)(MaxExtentScale * (float)(rcBody.left + rcBody.right)) + iPadding / 2; + if(rcPreview.left < iMaxExtent) rcPreview.left = iMaxExtent; + rcPreview.bottom = rcBody.bottom; + rcPreview.right = rcBody.right; + + // Re-layout the preview box + _mutex->lock(); + + _preview_width = rcPreview.right - rcPreview.left; + _preview_height = rcPreview.bottom - rcPreview.top; + + _mutex->unlock(); + + render_preview(); + + MoveWindow(_preview_wnd, rcPreview.left, rcPreview.top, + _preview_width, _preview_height, TRUE); + + rcFileList.r.right = rcPreview.left - iPadding; + } + + // Re-layout the file list box + MoveWindow(hFileListWnd, rcFileList.r.left, rcFileList.r.top, + rcFileList.r.right - rcFileList.r.left, + rcFileList.r.bottom - rcFileList.r.top, TRUE); + + // Re-layout the toolbar + RECTPOINTS rcToolBar; + GetWindowRect(_toolbar_wnd, &rcToolBar.r); + MapWindowPoints(NULL, _file_dialog_wnd, rcToolBar.p, 2); + MoveWindow(_toolbar_wnd, rcToolBar.r.left, rcToolBar.r.top, + rcToolBar.r.right - rcToolBar.r.left, rcToolBar.r.bottom - rcToolBar.r.top, TRUE); +} + +void FileOpenDialogImplWin32::file_selected() +{ + // Destroy any previous previews + free_preview(); + + + // Determine if the file exists + DWORD attributes = GetFileAttributesW(_path_string); + if(attributes == 0xFFFFFFFF || + attributes == FILE_ATTRIBUTE_DIRECTORY) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Check the file exists and get the file size + HANDLE file_handle = CreateFileW(_path_string, GENERIC_READ, + FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if(file_handle == INVALID_HANDLE_VALUE) return; + const DWORD file_size = GetFileSize(file_handle, NULL); + if (file_size == INVALID_FILE_SIZE) return; + _preview_file_size = file_size / 1024; + CloseHandle(file_handle); + + if(_show_preview) load_preview(); +} + +void FileOpenDialogImplWin32::load_preview() +{ + // Destroy any previous previews + free_preview(); + + // Try to get the file icon + SHFILEINFOW fileInfo; + if(SUCCEEDED(SHGetFileInfoW(_path_string, 0, &fileInfo, + sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON))) + _preview_file_icon = fileInfo.hIcon; + + // Will this file be too big? + if(_preview_file_size > MaxPreviewFileSize) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Prepare to render a preview + const Glib::ustring svg = ".svg"; + const Glib::ustring svgz = ".svgz"; + const Glib::ustring emf = ".emf"; + const Glib::ustring wmf = ".wmf"; + const Glib::ustring path = utf16_to_ustring(_path_string); + + bool success = false; + + _preview_document_width = _preview_document_height = 0; + + if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) && + (hasSuffix(path, svg) || hasSuffix(path, svgz))) + success = set_svg_preview(); + else if (hasSuffix(path, emf) || hasSuffix(path, wmf)) + success = set_emf_preview(); + else if (isValidImageFile(path)) + success = set_image_preview(); + else { + // Show no preview + } + + if(success) render_preview(); + + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +void FileOpenDialogImplWin32::free_preview() +{ + _mutex->lock(); + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + if(_preview_file_icon != NULL) + DestroyIcon(_preview_file_icon); + _preview_file_icon = NULL; + + _preview_bitmap_image.reset(); + _preview_emf_image = false; + _mutex->unlock(); +} + +bool FileOpenDialogImplWin32::set_svg_preview() +{ + const int PreviewSize = 512; + + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)_path_string, + _MAX_PATH, NULL, NULL, NULL); + SPDocument *svgDoc = SPDocument::createNewDoc (utf8string, 0); + g_free(utf8string); + + // Check the document loaded properly + if (svgDoc == NULL) { + return false; + } + if (svgDoc->getRoot() == NULL) + { + svgDoc->doUnref(); + return false; + } + + // Get the size of the document + Inkscape::Util::Quantity svgWidth = svgDoc->getWidth(); + Inkscape::Util::Quantity svgHeight = svgDoc->getHeight(); + const double svgWidth_px = svgWidth.value("px"); + const double svgHeight_px = svgHeight.value("px"); + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = PreviewSize / svgWidth_px; + const double scaleFactorY = PreviewSize / svgHeight_px; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + // Now get the resized values + const int scaledSvgWidth = round(scaleFactor * svgWidth_px); + const int scaledSvgHeight = round(scaleFactor * svgHeight_px); + + const double dpi = 96*scaleFactor; + Inkscape::Pixbuf * pixbuf = sp_generate_internal_bitmap(svgDoc, NULL, 0, 0, svgWidth_px, svgHeight_px, scaledSvgWidth, scaledSvgHeight, dpi, dpi, (guint32) 0xffffff00, NULL); + + // Tidy up + svgDoc->doUnref(); + if (pixbuf == NULL) { + return false; + } + + // Create the GDK pixbuf + _mutex->lock(); + _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw()); + _preview_document_width = svgWidth_px; + _preview_document_height = svgHeight_px; + _preview_image_width = scaledSvgWidth; + _preview_image_height = scaledSvgHeight; + _mutex->unlock(); + + return true; +} + +void FileOpenDialogImplWin32::destroy_svg_rendering(const guint8 *buffer) +{ + g_assert(buffer != NULL); + g_free((void*)buffer); +} + +bool FileOpenDialogImplWin32::set_image_preview() +{ + const Glib::ustring path = utf16_to_ustring(_path_string, _MAX_PATH); + + bool successful = false; + + _mutex->lock(); + + try { + _preview_bitmap_image = Gdk::Pixbuf::create_from_file(path); + if (_preview_bitmap_image) { + _preview_image_width = _preview_bitmap_image->get_width(); + _preview_document_width = _preview_image_width; + _preview_image_height = _preview_bitmap_image->get_height(); + _preview_document_height = _preview_image_height; + successful = true; + } + } + catch (const Gdk::PixbufError&) {} + catch (const Glib::FileError&) {} + + _mutex->unlock(); + + return successful; +} + +// Aldus Placeable Header =================================================== +// Since we are a 32bit app, we have to be sure this structure compiles to +// be identical to a 16 bit app's version. To do this, we use the #pragma +// to adjust packing, we use a WORD for the hmf handle, and a SMALL_RECT +// for the bbox rectangle. +#pragma pack( push ) +#pragma pack( 2 ) +struct APMHEADER +{ + DWORD dwKey; + WORD hmf; + SMALL_RECT bbox; + WORD wInch; + DWORD dwReserved; + WORD wCheckSum; +}; +using PAPMHEADER = APMHEADER *; +#pragma pack( pop ) + + +static HENHMETAFILE +MyGetEnhMetaFileW( const WCHAR *filename ) +{ + // Try open as Enhanced Metafile + HENHMETAFILE hemf = GetEnhMetaFileW(filename); + + if (!hemf) { + // Try open as Windows Metafile + HMETAFILE hmf = GetMetaFileW(filename); + + METAFILEPICT mp; + HDC hDC; + + if (!hmf) { + WCHAR szTemp[MAX_PATH]; + + DWORD dw = GetShortPathNameW( filename, szTemp, MAX_PATH ); + if (dw) { + hmf = GetMetaFileW( szTemp ); + } + } + + if (hmf) { + // Convert Windows Metafile to Enhanced Metafile + DWORD nSize = GetMetaFileBitsEx( hmf, 0, NULL ); + + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = GetMetaFileBitsEx( hmf, nSize, lpvData ); + if (dw) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = 1000; + mp.yExt = 1000; + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Make an enhanced metafile from the windows metafile + hemf = SetWinMetaFileBits( nSize, lpvData, hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + DeleteMetaFile( hmf ); + } + delete[] lpvData; + } + else { + DeleteMetaFile( hmf ); + } + } + else { + DeleteMetaFile( hmf ); + } + } + else { + // Try open as Aldus Placeable Metafile + HANDLE hFile; + hFile = CreateFileW( filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); + + if (hFile != INVALID_HANDLE_VALUE) { + DWORD nSize = GetFileSize( hFile, NULL ); + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = ReadFile( hFile, lpvData, nSize, &nSize, NULL ); + if (dw) { + if ( ((PAPMHEADER)lpvData)->dwKey == 0x9ac6cdd7l ) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = ((PAPMHEADER)lpvData)->bbox.Right - ((PAPMHEADER)lpvData)->bbox.Left; + mp.xExt = ( mp.xExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.yExt = ((PAPMHEADER)lpvData)->bbox.Bottom - ((PAPMHEADER)lpvData)->bbox.Top; + mp.yExt = ( mp.yExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Create an enhanced metafile from the bits + hemf = SetWinMetaFileBits( nSize, lpvData+sizeof(APMHEADER), hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + } + } + delete[] lpvData; + } + } + CloseHandle( hFile ); + } + } + } + + return hemf; +} + + +bool FileOpenDialogImplWin32::set_emf_preview() +{ + _mutex->lock(); + + BOOL ok = FALSE; + + DWORD w = 0; + DWORD h = 0; + + HENHMETAFILE hemf = MyGetEnhMetaFileW( _path_string ); + + if (hemf) + { + ENHMETAHEADER emh; + ZeroMemory(&emh, sizeof(emh)); + ok = GetEnhMetaFileHeader(hemf, sizeof(emh), &emh) != 0; + + w = (emh.rclFrame.right - emh.rclFrame.left); + h = (emh.rclFrame.bottom - emh.rclFrame.top); + + DeleteEnhMetaFile(hemf); + } + + if (ok) + { + const int PreviewSize = 512; + + // Get the size of the document + const double emfWidth = w; + const double emfHeight = h; + + _preview_document_width = emfWidth / 2540 * 96; // width is in units of 0.01 mm + _preview_document_height = emfHeight / 2540 * 96; // height is in units of 0.01 mm + _preview_image_width = emfWidth; + _preview_image_height = emfHeight; + + _preview_emf_image = true; + } + + _mutex->unlock(); + + return ok; +} + +void FileOpenDialogImplWin32::render_preview() +{ + double x, y; + const double blurRadius = 8; + const double halfBlurRadius = blurRadius / 2; + const int shaddowOffsetX = 0; + const int shaddowOffsetY = 2; + const int pagePadding = 5; + const double shaddowAlpha = 0.75; + + // Is the preview showing? + if(!_show_preview) + return; + + // Do we have anything to render? + _mutex->lock(); + + if(!_preview_bitmap_image && !_preview_emf_image) + { + _mutex->unlock(); + return; + } + + // Tidy up any previous bitmap renderings + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + // Calculate the size of the caption + int captionHeight = 0; + + if(_preview_wnd != NULL) + { + RECT rcCaptionRect; + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = format_caption(szCaption, + sizeof(szCaption) / sizeof(WCHAR)); + + HDC dc = GetDC(_preview_wnd); + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS | DT_CALCRECT); + ReleaseDC(_preview_wnd, dc); + + captionHeight = rcCaptionRect.bottom - rcCaptionRect.top; + } + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = ((double)_preview_width - pagePadding * 2 - blurRadius) / _preview_image_width; + const double scaleFactorY = ((double)_preview_height - pagePadding * 2 - shaddowOffsetY - halfBlurRadius - captionHeight) / _preview_image_height; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + // Now get the resized values + const double scaledSvgWidth = scaleFactor * _preview_image_width; + const double scaledSvgHeight = scaleFactor * _preview_image_height; + + const int svgX = pagePadding + halfBlurRadius; + const int svgY = pagePadding; + + const int frameX = svgX - pagePadding; + const int frameY = svgY - pagePadding; + const int frameWidth = scaledSvgWidth + pagePadding * 2; + const int frameHeight = scaledSvgHeight + pagePadding * 2; + + const int totalWidth = (int)ceil(frameWidth + blurRadius); + const int totalHeight = (int)ceil(frameHeight + blurRadius); + + // Prepare the drawing surface + HDC hDC = GetDC(_preview_wnd); + HDC hMemDC = CreateCompatibleDC(hDC); + _preview_bitmap = CreateCompatibleBitmap(hDC, totalWidth, totalHeight); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, _preview_bitmap); + Cairo::RefPtr surface = Win32Surface::create(hMemDC); + Cairo::RefPtr context = Context::create(surface); + + // Paint the background to match the dialog colour + const COLORREF background = GetSysColor(COLOR_3DFACE); + context->set_source_rgb( + GetRValue(background) / 255.0, + GetGValue(background) / 255.0, + GetBValue(background) / 255.0); + context->paint(); + + //----- Draw the drop shadow -----// + + // Left Edge + x = frameX + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr leftEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + leftEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + leftEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(leftEdgeFade); + context->rectangle (x, frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Right Edge + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr rightEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + rightEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + rightEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(rightEdgeFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Top Edge + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr topEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + topEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + topEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(topEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Bottom Edge + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr bottomEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + bottomEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Top Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr topLeftCornerFade = RadialGradient::create( + x + blurRadius, y + blurRadius, 0, x + blurRadius, y + blurRadius, blurRadius); + topLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Top Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr topRightCornerFade = RadialGradient::create( + x, y + blurRadius, 0, x, y + blurRadius, blurRadius); + topRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topRightCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr bottomLeftCornerFade = RadialGradient::create( + x + blurRadius, y, 0, x + blurRadius, y, blurRadius); + bottomLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr bottomRightCornerFade = RadialGradient::create( + x, y, 0, x, y, blurRadius); + bottomRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomRightCornerFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + frameHeight + shaddowOffsetY - halfBlurRadius, + blurRadius, blurRadius); + context->fill(); + + // Draw the frame + context->set_line_width(1); + context->rectangle (frameX, frameY, frameWidth, frameHeight); + + context->set_source_rgb(1.0, 1.0, 1.0); + context->fill_preserve(); + context->set_source_rgb(0.25, 0.25, 0.25); + context->stroke_preserve(); + + // Draw the image + if(_preview_bitmap_image) // Is the image a pixbuf? + { + // Set the transformation + const Cairo::Matrix matrix( + scaleFactor, 0, + 0, scaleFactor, + svgX, svgY); + context->set_matrix (matrix); + + // Render the image + set_source_pixbuf (context, _preview_bitmap_image, 0, 0); + context->paint(); + + // Reset the transformation + context->set_identity_matrix(); + } + + // Draw the inner frame + context->set_source_rgb(0.75, 0.75, 0.75); + context->rectangle (svgX, svgY, scaledSvgWidth, scaledSvgHeight); + context->stroke(); + + _mutex->unlock(); + + // Finish drawing + surface->finish(); + + if (_preview_emf_image) { + HENHMETAFILE hemf = MyGetEnhMetaFileW(_path_string); + if (hemf) { + RECT rc; + rc.top = svgY+2; + rc.left = svgX+2; + rc.bottom = scaledSvgHeight-2; + rc.right = scaledSvgWidth-2; + PlayEnhMetaFile(hMemDC, hemf, &rc); + DeleteEnhMetaFile(hemf); + } + } + + SelectObject(hMemDC, hOldBitmap) ; + DeleteDC(hMemDC); + + // Refresh the preview pane + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +int FileOpenDialogImplWin32::format_caption(wchar_t *caption, int caption_size) +{ + wchar_t szFileName[_MAX_FNAME]; + _wsplitpath(_path_string, NULL, NULL, szFileName, NULL); + + return snwprintf(caption, caption_size, + L"%ls\n%d\u2009kB\n%d\u2009px \xD7 %d\u2009px", szFileName, _preview_file_size, + (int)_preview_document_width, (int)_preview_document_height); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileOpenDialogImplWin32::show() +{ + // We can only run one worker thread at a time + if(_mutex != NULL) return false; + +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _finished = false; + _file_selected = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + +#if GLIB_CHECK_VERSION(2,32,0) + _mutex = new Glib::Threads::Mutex(); + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread))) +#else + _mutex = new Glib::Mutex(); + if(Glib::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread), true)) +#endif + { + while(1) + { + g_main_context_iteration(g_main_context_default(), FALSE); + + if(_mutex->trylock()) + { + // Read mutexed data + const bool finished = _finished; + const bool is_file_selected = _file_selected; + _file_selected = false; + _mutex->unlock(); + + if(finished) break; + if(is_file_selected) file_selected(); + } + + Sleep(10); + } + } + + // Tidy up + delete _mutex; + _mutex = NULL; + + return _result; +} + +/** + * To Get Multiple filenames selected at-once. + */ +std::vectorFileOpenDialogImplWin32::getFilenames() +{ + std::vector result; + result.push_back(getFilename()); + return result; +} + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/** + * Constructor + */ +FileSaveDialogImplWin32::FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &/*default_key*/, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) : + FileDialogBaseWin32(parent, dir, title, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "dialogs.save_copy" : "dialogs.save_as"), + _title_label(NULL), + _title_edit(NULL) +{ + FileSaveDialog::myDocTitle = docTitle; + createFilterMenu(); + + /* The code below sets the default file name */ + myFilename = ""; + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') udir.erase(len - 1); + + // Remove the extension: remove everything past the last period found past the last slash + // (not for CUSTOM_TYPE as we can not automatically add a file extension in that case yet) + if (dialogType == CUSTOM_TYPE) { + myFilename = udir; + } else { + size_t last_slash_index = udir.find_last_of( '\\' ); + size_t last_period_index = udir.find_last_of( '.' ); + if (last_period_index > last_slash_index) { + myFilename = udir.substr(0, last_period_index ); + } + } + + // remove one slash if double + if (1 + myFilename.find("\\\\",2)) { + myFilename.replace(myFilename.find("\\\\",2), 1, ""); + } + } +} + +FileSaveDialogImplWin32::~FileSaveDialogImplWin32() +{ +} + +void FileSaveDialogImplWin32::createFilterMenu() +{ + std::list filter_list; + + knownExtensions.clear(); + + // Compose the filter string + Glib::ustring all_inkscape_files_filter, all_image_files_filter; + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + + int filter_count = 0; + int filter_length = 1; + + for (Inkscape::Extension::DB::OutputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Inkscape::Extension::Output *omod = *current_item; + if (omod->deactivated()) continue; + + filter_count++; + + Filter filter; + + // Extension + const gchar *filter_extension = omod->get_extension(); + filter.filter = g_utf8_to_utf16( + filter_extension, -1, NULL, &filter.filter_length, NULL); + knownExtensions.insert(std::pair(Glib::ustring(filter_extension).casefold(), omod)); + + // Type + filter.name = g_utf8_to_utf16(omod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + filter.mod = omod; + + filter_length += filter.name_length + + filter.filter_length + 3; // Add 3 for two \0s and a * + + filter_list.push_back(filter); + } + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = 0; + + _filter_count = extension_index; + _filter_index = 1; // A value of 1 selects the 1st filter - NOT the 2nd +} + + +void FileSaveDialogImplWin32::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + std::list filter_list; + + knownExtensions.clear(); + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + knownExtensions.insert(std::pair(Glib::ustring(all_exe_files_filter).casefold(), NULL)); + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list + + +} + +void FileSaveDialogImplWin32::GetSaveFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_main_loop != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.nFilterIndex = _filter_index; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetSaveFileName_hookproc; + ofn.lpstrDefExt = L"svg\0"; + ofn.lCustData = (LPARAM)this; + _result = GetSaveFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + g_main_loop_quit(_main_loop); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileSaveDialogImplWin32::show() +{ +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + + if(_main_loop != NULL) + { +#if GLIB_CHECK_VERSION(2,32,0) + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread))) +#else + if(Glib::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread), true)) +#endif + g_main_loop_run(_main_loop); + + if(_result && _extension) + appendExtension(myFilename, (Inkscape::Extension::Output*)_extension); + } + + return _result; +} + +void FileSaveDialogImplWin32::setSelectionType( Inkscape::Extension::Extension * /*key*/ ) +{ + // If no pointer to extension is passed in, look up based on filename extension. + +} + + +UINT_PTR CALLBACK FileSaveDialogImplWin32::GetSaveFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileSaveDialogImplWin32 *pImpl = reinterpret_cast + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // get size/pos of typical combo box + RECT rEDT1, rCB1, rROOT, rST; + GetWindowRect(GetDlgItem(hParentWnd, cmb1), &rCB1); + GetWindowRect(GetDlgItem(hParentWnd, cmb13), &rEDT1); + GetWindowRect(GetDlgItem(hParentWnd, stc2), &rST); + GetWindowRect(hdlg, &rROOT); + int ydelta = rCB1.top - rEDT1.top; + if ( ydelta < 0 ) { + g_warning("Negative dialog ydelta"); + ydelta = 0; + } + + // Make the window a bit longer + // Note: we have a width delta of 1 because there is a suspicion that MoveWindow() to the same size causes zero-width results. + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + sanitizeWindowSizeParam( rcRect.right - rcRect.left, 1, WINDOW_WIDTH_MINIMUM, WINDOW_WIDTH_FALLBACK ), + sanitizeWindowSizeParam( rcRect.bottom - rcRect.top, ydelta, WINDOW_HEIGHT_MINIMUM, WINDOW_HEIGHT_FALLBACK ), + FALSE); + + // It is not necessary to delete stock objects by calling DeleteObject + HGDIOBJ dlgFont = GetStockObject(DEFAULT_GUI_FONT); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast(ofn->lCustData); + + // Create the Title label and edit control + wchar_t *title_label_str = (wchar_t *)g_utf8_to_utf16(_("Title:"), -1, NULL, NULL, NULL); + pImpl->_title_label = CreateWindowExW(0, L"STATIC", title_label_str, + WS_VISIBLE|WS_CHILD, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.left-rST.left, rST.bottom-rST.top, + hParentWnd, NULL, hInstance, NULL); + g_free(title_label_str); + + if(pImpl->_title_label) { + if(dlgFont) SendMessage(pImpl->_title_label, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_label, NULL, rST.left-rROOT.left, rST.top+ydelta-rROOT.top, + rCB1.left-rST.left, rST.bottom-rST.top, SWP_SHOWWINDOW|SWP_NOZORDER); + } + + pImpl->_title_edit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", + WS_VISIBLE|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, + hParentWnd, NULL, hInstance, NULL); + if(pImpl->_title_edit) { + if(dlgFont) SendMessage(pImpl->_title_edit, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_edit, NULL, rCB1.left-rROOT.left, rCB1.top+ydelta-rROOT.top, + rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, SWP_SHOWWINDOW|SWP_NOZORDER); + SetWindowTextW(pImpl->_title_edit, + (const wchar_t*)g_utf8_to_utf16(pImpl->myDocTitle.c_str(), -1, NULL, NULL, NULL)); + } + } + break; + case WM_DESTROY: + { + if(pImpl->_title_edit) { + int length = GetWindowTextLengthW(pImpl->_title_edit)+1; + wchar_t* temp_title = new wchar_t[length]; + GetWindowTextW(pImpl->_title_edit, temp_title, length); + pImpl->myDocTitle = g_utf16_to_utf8((gunichar2*)temp_title, -1, NULL, NULL, NULL); + delete[] temp_title; + DestroyWindow(pImpl->_title_label); + pImpl->_title_label = NULL; + DestroyWindow(pImpl->_title_edit); + pImpl->_title_edit = NULL; + } + } + break; + } + + // Use default dialog behaviour + return 0; +} + +} } } // namespace Dialog, UI, Inkscape + +#endif // ifdef _WIN32 + +/* + 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 : diff --git a/src/ui/dialog/filedialogimpl-win32.h b/src/ui/dialog/filedialogimpl-win32.h new file mode 100644 index 0000000..9dedaa3 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.h @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of native file dialogs for Win32 + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#ifdef _WIN32 + +#include "filedialogimpl-gtkmm.h" + +#include "inkgc/gc-core.h" + +#include + + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/// This class is the base implementation of a MS Windows +/// file dialog. +class FileDialogBaseWin32 +{ +protected: + /// Abstract Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + /// @param preferenceBase The preferences key + FileDialogBaseWin32(Gtk::Window &parent, const Glib::ustring &dir, + const char *title, FileDialogType type, + gchar const *preferenceBase); + + /// Destructor + ~FileDialogBaseWin32(); + +public: + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + Inkscape::Extension::Extension* getSelectionType(); + + /// Get the path of the current directory + Glib::ustring getCurrentDirectory(); + + +protected: + /// The dialog type + FileDialogType dialogType; + + /// A pointer to the GTK main-loop context object. This + /// is used to keep the rest of the inkscape UI running + /// while the file dialog is displayed + GMainLoop *_main_loop; + + /// The result of the call to GetOpenFileName. If true + /// the user clicked OK, if false the user clicked cancel + bool _result; + + /// The parent window + Gtk::Window &parent; + + /// The windows handle of the parent window + HWND _ownerHwnd; + + /// The path of the directory that is currently being + /// browsed + Glib::ustring _current_directory; + + /// The title of the dialog in UTF-16 + wchar_t *_title; + + /// The path of the currently selected file in UTF-16 + wchar_t _path_string[_MAX_PATH]; + + /// The filter string for GetOpenFileName in UTF-16 + wchar_t *_filter; + + /// The index of the currently selected filter. + /// This value must be greater than or equal to 1, + /// and less than or equal to _filter_count. + unsigned int _filter_index; + + /// The number of filters registered + unsigned int _filter_count; + + /// An array of the extensions associated with the + /// file types of each filter. So the Nth entry of + /// this array corresponds to the extension of the Nth + /// filter in the list. NULL if no specific extension is + /// specified/ + Inkscape::Extension::Extension **_extension_map; + + /// The currently selected extension. Valid after an [OK] + Inkscape::Extension::Extension *_extension; +}; + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetOpenFileName API +class FileOpenDialogImplWin32 : public FileOpenDialog, public FileDialogBaseWin32 +{ +public: + /// Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title); + + /// Destructor + virtual ~FileOpenDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Gets a list of the selected file names + /// @return Returns an STL vector filled with the + /// GTK names of the selected files + std::vector getFilenames(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering patter (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern); + +private: + + /// Create filter menu for this type of dialog + void createFilterMenu(); + + + /// The handle of the preview pane window + HWND _preview_wnd; + + /// The handle of the file dialog window + HWND _file_dialog_wnd; + + /// A pointer to the standard window proc of the + /// unhooked file dialog + WNDPROC _base_window_proc; + + /// The handle of the bitmap of the "show preview" + /// toggle button + HBITMAP _show_preview_button_bitmap; + + /// The handle of the toolbar's window + HWND _toolbar_wnd; + + /// This flag is set true when the preview should be + /// shown, or false when it should be hidden + static bool _show_preview; + + + /// The current width of the preview pane in pixels + int _preview_width; + + /// The current height of the preview pane in pixels + int _preview_height; + + /// The handle of the windows to display within the + /// preview pane, or NULL if no image should be displayed + HBITMAP _preview_bitmap; + + /// The windows shell icon for the selected file + HICON _preview_file_icon; + + /// The size of the preview file in kilobytes + unsigned long _preview_file_size; + + + /// The width of the document to be shown in the preview panel + double _preview_document_width; + + /// The width of the document to be shown in the preview panel + double _preview_document_height; + + /// The width of the rendered preview image in pixels + int _preview_image_width; + + /// The height of the rendered preview image in pixels + int _preview_image_height; + + /// A GDK Pixbuf of the rendered preview to be displayed + Glib::RefPtr _preview_bitmap_image; + + /// This flag is set true if a file has been selected + bool _file_selected; + + /// This flag is set true when the GetOpenFileName call + /// has returned + bool _finished; + + /// This mutex is used to ensure that the worker thread + /// that calls GetOpenFileName cannot collide with the + /// main Inkscape thread +#if GLIB_CHECK_VERSION(2,32,0) + Glib::Threads::Mutex *_mutex; +#else + Glib::Mutex *_mutex; +#endif + + + /// The controller function for the thread which calls + /// GetOpenFileName + void GetOpenFileName_thread(); + + /// Registers the Windows Class of the preview panel window + static void register_preview_wnd_class(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetOpenFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + + /// A message proc which wraps the standard dialog proc, + /// but intercepts some calls + static LRESULT CALLBACK file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// The message proc for the preview panel window + static LRESULT CALLBACK preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// Lays out the controls in the file dialog given it's + /// current size + /// GetOpenFileName thread only. + void layout_dialog(); + + /// Enables or disables the file preview. + /// GetOpenFileName thread only. + void enable_preview(bool enable); + + /// This function is called in the App thread when a file had + /// been selected + void file_selected(); + + /// Loads and renders the unshrunk preview image. + /// Main app thread only. + void load_preview(); + + /// Frees all the allocated objects associated with the file + /// currently being previewed + /// Main app thread only. + void free_preview(); + + /// Loads preview for an SVG or SVGZ file. + /// Main app thread only. + /// @return Returns true if the SVG loaded successfully + bool set_svg_preview(); + + /// A callback to allow this class to dispose of the + /// memory block of the rendered SVG bitmap + /// @buffer buffer The buffer to free + static void destroy_svg_rendering(const guint8 *buffer); + + /// Loads the preview for a raster image + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_image_preview(); + + /// Loads the preview for a meta file + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_emf_preview(); + + /// This flag is set true when a meta file is previewed + bool _preview_emf_image; + + /// Renders the unshrunk preview image to a windows HTBITMAP + /// which can be painted in the preview pain. + /// Main app thread only. + void render_preview(); + + /// Formats the caption in UTF-16 for the preview image + /// @param caption The buffer to format the caption string into + /// @param caption_size The number of wchar_ts in the caption buffer + /// @return Returns the number of characters in caption string + int format_caption(wchar_t *caption, int caption_size); +}; + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetSaveFileName API +class FileSaveDialogImplWin32 : public FileSaveDialog, public FileDialogBaseWin32 +{ + +public: + FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + /// Destructor + virtual ~FileSaveDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + virtual void setSelectionType( Inkscape::Extension::Extension *key ); + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern); + +private: + /// A handle to the title label and edit box + HWND _title_label; + HWND _title_edit; + + /// Create a filter menu for this type of dialog + void createFilterMenu(); + + /// The controller function for the thread which calls + /// GetSaveFileName + void GetSaveFileName_thread(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetSaveFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + +}; + + +} +} +} + +#endif + +/* + 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 : diff --git a/src/ui/dialog/fill-and-stroke.cpp b/src/ui/dialog/fill-and-stroke.cpp new file mode 100644 index 0000000..1de7ede --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill and Stroke dialog - implementation. + * + * Based on the old sp_object_properties_dialog. + */ +/* Authors: + * Bryce W. Harrington + * Gustav Broberg + * Jon A. Cruz + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "desktop-style.h" +#include "document.h" +#include "fill-and-stroke.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "preferences.h" +#include "verbs.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/notebook-page.h" + +#include "widgets/fill-style.h" +#include "widgets/paint-selector.h" +#include "widgets/stroke-style.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FillAndStroke::FillAndStroke() + : UI::Widget::Panel("/dialogs/fillstroke", SP_VERB_DIALOG_FILL_STROKE) + , _page_fill(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_paint(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_style(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _composite_settings(SP_VERB_DIALOG_FILL_STROKE, "fillstroke", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY) + , deskTrack() + , targetDesktop(nullptr) + , fillWdgt(nullptr) + , strokeWdgt(nullptr) + , desktopChangeConn() +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(2); + contents->pack_start(_notebook, true, true); + + _notebook.append_page(*_page_fill, _createPageTabLabel(_("_Fill"), INKSCAPE_ICON("object-fill"))); + _notebook.append_page(*_page_stroke_paint, _createPageTabLabel(_("Stroke _paint"), INKSCAPE_ICON("object-stroke"))); + _notebook.append_page(*_page_stroke_style, _createPageTabLabel(_("Stroke st_yle"), INKSCAPE_ICON("object-stroke-style"))); + _notebook.set_vexpand(true); + + _notebook.signal_switch_page().connect(sigc::mem_fun(this, &FillAndStroke::_onSwitchPage)); + + _layoutPageFill(); + _layoutPageStrokePaint(); + _layoutPageStrokeStyle(); + + contents->pack_end(_composite_settings, Gtk::PACK_SHRINK); + + show_all_children(); + + _composite_settings.setSubject(&_subject); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FillAndStroke::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +FillAndStroke::~FillAndStroke() +{ + _composite_settings.setSubject(nullptr); + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void FillAndStroke::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void FillAndStroke::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + targetDesktop = desktop; + if (fillWdgt) { + sp_fill_style_widget_set_desktop(fillWdgt, desktop); + } + if (strokeWdgt) { + sp_fill_style_widget_set_desktop(strokeWdgt, desktop); + } + if (strokeStyleWdgt) { + sp_stroke_style_widget_set_desktop(strokeStyleWdgt, desktop); + } + _composite_settings.setSubject(&_subject); + } +} + +void FillAndStroke::_onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + _savePagePref(pagenum); +} + +void +FillAndStroke::_savePagePref(guint page_num) +{ + // remember the current page + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/fillstroke/page", page_num); +} + +void +FillAndStroke::_layoutPageFill() +{ + fillWdgt = Gtk::manage(sp_fill_style_widget_new()); + _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokePaint() +{ + strokeWdgt = Gtk::manage(sp_stroke_style_paint_widget_new()); + _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokeStyle() +{ + strokeStyleWdgt = sp_stroke_style_line_widget_new(); + strokeStyleWdgt->set_hexpand(); + strokeStyleWdgt->set_halign(Gtk::ALIGN_START); + + _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::showPageFill() +{ + present(); + _notebook.set_current_page(0); + _savePagePref(0); + +} + +void +FillAndStroke::showPageStrokePaint() +{ + present(); + _notebook.set_current_page(1); + _savePagePref(1); +} + +void +FillAndStroke::showPageStrokeStyle() +{ + present(); + _notebook.set_current_page(2); + _savePagePref(2); + +} + +Gtk::HBox& +FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 4)); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +} // 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 : diff --git a/src/ui/dialog/fill-and-stroke.h b/src/ui/dialog/fill-and-stroke.h new file mode 100644 index 0000000..f425969 --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill and Stroke dialog + */ +/* Authors: + * Bryce W. Harrington + * Gustav Broberg + * Jon A. Cruz + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H +#define INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H + +#include "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "ui/dialog/desktop-tracker.h" + +#include +#include "ui/widget/style-subject.h" + +namespace Inkscape { +namespace UI { + +namespace Widget { +class NotebookPage; +} + +namespace Dialog { + +class FillAndStroke : public UI::Widget::Panel { +public: + FillAndStroke(); + ~FillAndStroke() override; + + static FillAndStroke &getInstance() { return *new FillAndStroke(); } + + + void setDesktop(SPDesktop *desktop) override; + + //void selectionChanged(Inkscape::Selection *selection); + + void showPageFill(); + void showPageStrokePaint(); + void showPageStrokeStyle(); + +protected: + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_fill; + UI::Widget::NotebookPage *_page_stroke_paint; + UI::Widget::NotebookPage *_page_stroke_style; + + UI::Widget::StyleSubject::Selection _subject; + UI::Widget::ObjectCompositeSettings _composite_settings; + + Gtk::HBox &_createPageTabLabel(const Glib::ustring &label, + const char *label_image); + + void _layoutPageFill(); + void _layoutPageStrokePaint(); + void _layoutPageStrokeStyle(); + void _savePagePref(guint page_num); + void _onSwitchPage(Gtk::Widget *page, guint pagenum); + +private: + FillAndStroke(FillAndStroke const &d) = delete; + FillAndStroke& operator=(FillAndStroke const &d) = delete; + + void setTargetDesktop(SPDesktop *desktop); + + DesktopTracker deskTrack; + SPDesktop *targetDesktop; + Gtk::Widget *fillWdgt; + Gtk::Widget *strokeWdgt; + Gtk::Widget *strokeStyleWdgt; + sigc::connection desktopChangeConn; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif // INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H + +/* + 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 : diff --git a/src/ui/dialog/filter-editor.cpp b/src/ui/dialog/filter-editor.cpp new file mode 100644 index 0000000..0fae983 --- /dev/null +++ b/src/ui/dialog/filter-editor.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "desktop.h" +#include "dialog-manager.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-editor.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "io/sys.h" +#include "io/resource.h" + +#include "object/filters/blend.h" +#include "object/filters/colormatrix.h" +#include "object/filters/componenttransfer.h" +#include "object/filters/componenttransfer-funcnode.h" +#include "object/filters/convolvematrix.h" +#include "object/filters/distantlight.h" +#include "object/filters/merge.h" +#include "object/filters/mergenode.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" +#include "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/widget/spinbutton.h" + +using namespace Inkscape::Filters; +using namespace Inkscape::IO::Resource; +namespace Inkscape { +namespace UI { +namespace Dialog { + +FilterEditorDialog::FilterEditorDialog() : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS) +{ + + const std::string req_widgets[] = {"FilterEditor", "FilterList", "FilterFERX", "FilterFERY", "FilterFERH", "FilterFERW", "FilterPreview", "FilterPrimitiveDescImage", "FilterPrimitiveList", "FilterPrimitiveDescText", "FilterPrimitiveAdd"}; + Glib::ustring gladefile = get_filename(UIS, "dialog-filter-editor.glade"); + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch(const Glib::Error& ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + + Gtk::Object* test; + for(std::string w:req_widgets) { + builder->get_widget(w,test); + if(!test){ + g_warning("Required widget %s does not exist", w.c_str()); + return; + } + } + + builder->get_widget("FilterEditor", FilterEditor); + _getContents()->add(*FilterEditor); + +//test + Gtk::ComboBox *OptionList; + builder->get_widget("OptionList",OptionList); + FilterStore = builder->get_object("FilterStore"); + Glib::RefPtr fs = Glib::RefPtr::cast_static(FilterStore); + Gtk::TreeModel::Row row = *(fs->append()); + + + + + +} +FilterEditorDialog::~FilterEditorDialog()= default; + + + + + + +} // Never put these namespaces together unless you are using gcc 6+ +} +} // P.S. This is for Inkscape::UI::Dialog + +/* + 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 : diff --git a/src/ui/dialog/filter-editor.h b/src/ui/dialog/filter-editor.h new file mode 100644 index 0000000..e20ecbf --- /dev/null +++ b/src/ui/dialog/filter-editor.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Editor dialog + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EDITOR_H +#define INKSCAPE_UI_DIALOG_FILTER_EDITOR_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ui/widget/panel.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FilterEditorDialog : public UI::Widget::Panel { +public: + + FilterEditorDialog(); + ~FilterEditorDialog() override; + + static FilterEditorDialog &getInstance() + { return *new FilterEditorDialog(); } + +// void set_attrs_locked(const bool); +private: + Glib::RefPtr builder; + Glib::RefPtr FilterStore; + Gtk::Box *FilterEditor; +}; +} +} +} +#endif diff --git a/src/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp new file mode 100644 index 0000000..cff7e3b --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -0,0 +1,3114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Nicholas Bishop + * Rodrigo Kumpera + * Felipe C. da S. Sanches + * Jon A. Cruz + * Abhishek Sharma + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "desktop.h" +#include "dialog-manager.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-effects-dialog.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/colormatrix.h" +#include "object/filters/componenttransfer.h" +#include "object/filters/componenttransfer-funcnode.h" +#include "object/filters/convolvematrix.h" +#include "object/filters/distantlight.h" +#include "object/filters/merge.h" +#include "object/filters/mergenode.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" +#include "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/widget/filter-effect-chooser.h" +#include "ui/widget/spinbutton.h" + +#include "io/sys.h" + + +using namespace Inkscape::Filters; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::AttrWidget; +using Inkscape::UI::Widget::ComboBoxEnum; +using Inkscape::UI::Widget::DualSpinScale; +using Inkscape::UI::Widget::SpinScale; + + +// Returns the number of inputs available for the filter primitive type +static int input_count(const SPFilterPrimitive* prim) +{ + if(!prim) + return 0; + else if(SP_IS_FEBLEND(prim) || SP_IS_FECOMPOSITE(prim) || SP_IS_FEDISPLACEMENTMAP(prim)) + return 2; + else if(SP_IS_FEMERGE(prim)) { + // Return the number of feMergeNode connections plus an extra + return (int) (prim->children.size() + 1); + } + else + return 1; +} + +class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget +{ +public: + CheckButtonAttr(bool def, const Glib::ustring& label, + Glib::ustring tv, Glib::ustring fv, + const SPAttributeEnum a, char* tip_text) + : Gtk::CheckButton(label), + AttrWidget(a, def), + _true_val(std::move(tv)), _false_val(std::move(fv)) + { + signal_toggled().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + Glib::ustring get_as_attribute() const override + { + return get_active() ? _true_val : _false_val; + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + if(_true_val == val) + set_active(true); + else if(_false_val == val) + set_active(false); + } else { + set_active(get_default()->as_bool()); + } + } +private: + const Glib::ustring _true_val, _false_val; +}; + +class SpinButtonAttr : public Inkscape::UI::Widget::SpinButton, public AttrWidget +{ +public: + SpinButtonAttr(double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, double def, char* tip_text) + : Inkscape::UI::Widget::SpinButton(climb_rate, digits), + AttrWidget(a, def) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + set_range(lower, upper); + set_increments(step_inc, 0); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + } + + Glib::ustring get_as_attribute() const override + { + const double val = get_value(); + + if(get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val){ + set_value(Glib::Ascii::strtod(val)); + } else { + set_value(get_default()->as_double()); + } + } +}; + +template< typename T> class ComboWithTooltip : public Gtk::EventBox +{ +public: + ComboWithTooltip(T default_value, const Util::EnumDataConverter& c, const SPAttributeEnum a = SP_ATTR_INVALID, char* tip_text = nullptr) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + combo = new ComboBoxEnum(default_value, c, a, false); + add(*combo); + show_all(); + } + + ~ComboWithTooltip() override + { + delete combo; + } + + ComboBoxEnum* get_attrwidget() + { + return combo; + } +private: + ComboBoxEnum* combo; +}; + +// Contains an arbitrary number of spin buttons that use separate attributes +class MultiSpinButton : public Gtk::HBox +{ +public: + MultiSpinButton(double lower, double upper, double step_inc, + double climb_rate, int digits, std::vector attrs, std::vector default_values, std::vector tip_text) + { + g_assert(attrs.size()==default_values.size()); + g_assert(attrs.size()==tip_text.size()); + set_spacing(4); + for(unsigned i = 0; i < attrs.size(); ++i) { + unsigned index = attrs.size() - 1 - i; + _spins.push_back(new SpinButtonAttr(lower, upper, step_inc, climb_rate, digits, attrs[index], default_values[index], tip_text[index])); + pack_end(*_spins.back(), false, false); + } + } + + ~MultiSpinButton() override + { + for(auto & _spin : _spins) + delete _spin; + } + + std::vector& get_spinbuttons() + { + return _spins; + } +private: + std::vector _spins; +}; + +// Contains two spinbuttons that describe a NumberOptNumber +class DualSpinButton : public Gtk::HBox, public AttrWidget +{ +public: + DualSpinButton(char* def, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, char* tt1, char* tt2) + : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor + _s1(climb_rate, digits), _s2(climb_rate, digits) + { + if (tt1) { + _s1.set_tooltip_text(tt1); + } + if (tt2) { + _s2.set_tooltip_text(tt2); + } + _s1.set_range(lower, upper); + _s2.set_range(lower, upper); + _s1.set_increments(step_inc, 0); + _s2.set_increments(step_inc, 0); + + _s1.signal_value_changed().connect(signal_attr_changed().make_slot()); + _s2.signal_value_changed().connect(signal_attr_changed().make_slot()); + + set_spacing(4); + pack_end(_s2, false, false); + pack_end(_s1, false, false); + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton1() + { + return _s1; + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton2() + { + return _s2; + } + + Glib::ustring get_as_attribute() const override + { + double v1 = _s1.get_value(); + double v2 = _s2.get_value(); + + if(_s1.get_digits() == 0) { + v1 = (int)v1; + v2 = (int)v2; + } + + return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + NumberOptNumber n; + if(val) { + n.set(val); + } else { + n.set(get_default()->as_charptr()); + } + _s1.set_value(n.getNumber()); + _s2.set_value(n.getOptNumber()); + + } +private: + Inkscape::UI::Widget::SpinButton _s1, _s2; +}; + +class ColorButton : public Gtk::ColorButton, public AttrWidget +{ +public: + ColorButton(unsigned int def, const SPAttributeEnum a, char* tip_text) + : AttrWidget(a, def) + { + signal_color_set().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + + Gdk::RGBA col; + col.set_rgba_u(65535, 65535, 65535); + set_rgba(col); + } + + // Returns the color in 'rgb(r,g,b)' form. + Glib::ustring get_as_attribute() const override + { + // no doubles here, so we can use the standard string stream. + std::ostringstream os; + + const auto c = get_rgba(); + const int r = c.get_red_u() / 257, g = c.get_green_u() / 257, b = c.get_blue_u() / 257;//TO-DO: verify this. This sounds a lot strange! shouldn't it be 256? + os << "rgb(" << r << "," << g << "," << b << ")"; + return os.str(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + guint32 i = 0; + if(val) { + i = sp_svg_read_color(val, 0xFFFFFFFF); + } else { + i = (guint32) get_default()->as_uint(); + } + const int r = SP_RGBA32_R_U(i), g = SP_RGBA32_G_U(i), b = SP_RGBA32_B_U(i); + + Gdk::RGBA col; + col.set_rgba_u(r * 256, g * 256, b * 256); + set_rgba(col); + } +}; + +// Used for tableValue in feComponentTransfer +class EntryAttr : public Gtk::Entry, public AttrWidget +{ +public: + EntryAttr(const SPAttributeEnum a, char* tip_text) + : AttrWidget(a) + { + signal_changed().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + // No validity checking is done + Glib::ustring get_as_attribute() const override + { + return get_text(); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + set_text( val ); + } else { + set_text( "" ); + } + } +}; + +/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */ +class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget +{ +public: + MatrixAttr(const SPAttributeEnum a, char* tip_text = nullptr) + : AttrWidget(a), _locked(false) + { + _model = Gtk::ListStore::create(_columns); + _tree.set_model(_model); + _tree.set_headers_visible(false); + _tree.show(); + add(_tree); + set_shadow_type(Gtk::SHADOW_IN); + if (tip_text) { + _tree.set_tooltip_text(tip_text); + } + } + + std::vector get_values() const + { + std::vector vec; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) + vec.push_back(iter[_columns.cols[c]]); + } + return vec; + } + + void set_values(const std::vector& v) + { + unsigned i = 0; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + if(i >= v.size()) + return; + iter[_columns.cols[c]] = v[i]; + ++i; + } + } + } + + Glib::ustring get_as_attribute() const override + { + // use SVGOStringStream to output SVG-compatible doubles + Inkscape::SVGOStringStream os; + + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + os << iter[_columns.cols[c]] << " "; + } + } + + return os.str(); + } + + void set_from_attribute(SPObject* o) override + { + if(o) { + if(SP_IS_FECONVOLVEMATRIX(o)) { + SPFeConvolveMatrix* conv = SP_FECONVOLVEMATRIX(o); + int cols, rows; + cols = (int)conv->order.getNumber(); + if(cols > 5) + cols = 5; + rows = conv->order.optNumber_set ? (int)conv->order.getOptNumber() : cols; + update(o, rows, cols); + } + else if(SP_IS_FECOLORMATRIX(o)) + update(o, 4, 5); + } + } +private: + class MatrixColumns : public Gtk::TreeModel::ColumnRecord + { + public: + MatrixColumns() + { + cols.resize(5); + for(auto & col : cols) + add(col); + } + std::vector > cols; + }; + + void update(SPObject* o, const int rows, const int cols) + { + if(_locked) + return; + + _model->clear(); + + _tree.remove_all_columns(); + + std::vector* values = nullptr; + if(SP_IS_FECOLORMATRIX(o)) + values = &SP_FECOLORMATRIX(o)->values; + else if(SP_IS_FECONVOLVEMATRIX(o)) + values = &SP_FECONVOLVEMATRIX(o)->kernelMatrix; + else + return; + + if(o) { + int ndx = 0; + + for(int i = 0; i < cols; ++i) { + _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f"); + dynamic_cast( + _tree.get_column_cell_renderer(i))->signal_edited().connect( + sigc::mem_fun(*this, &MatrixAttr::rebind)); + } + + for(int r = 0; r < rows; ++r) { + Gtk::TreeRow row = *(_model->append()); + // Default to identity matrix + for(int c = 0; c < cols; ++c, ++ndx) + row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0); + } + } + } + + void rebind(const Glib::ustring&, const Glib::ustring&) + { + _locked = true; + signal_attr_changed()(); + _locked = false; + } + + bool _locked; + Gtk::TreeView _tree; + Glib::RefPtr _model; + MatrixColumns _columns; +}; + +// Displays a matrix or a slider for feColorMatrix +class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget +{ +public: + ColorMatrixValues() + : AttrWidget(SP_ATTR_VALUES), + // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor + _matrix(SP_ATTR_VALUES, _("This matrix determines a linear transform on color space. Each line affects one of the color components. Each column determines how much of each color component from the input is passed to the output. The last column does not depend on input colors, so can be used to adjust a constant component value.")), + _saturation("", 0, 0, 1, 0.1, 0.01, 2, SP_ATTR_VALUES), + _angle("", 0, 0, 360, 0.1, 0.01, 1, SP_ATTR_VALUES), + _label(C_("Label", "None"), Gtk::ALIGN_START), + _use_stored(false), + _saturation_store(0), + _angle_store(0) + { + _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _angle.signal_attr_changed().connect(signal_attr_changed().make_slot()); + signal_attr_changed().connect(sigc::mem_fun(*this, &ColorMatrixValues::update_store)); + + _matrix.show(); + _saturation.show(); + _angle.show(); + _label.show(); + _label.set_sensitive(false); + + set_shadow_type(Gtk::SHADOW_NONE); + } + + void set_from_attribute(SPObject* o) override + { + if(SP_IS_FECOLORMATRIX(o)) { + SPFeColorMatrix* col = SP_FECOLORMATRIX(o); + remove(); + switch(col->type) { + case COLORMATRIX_SATURATE: + add(_saturation); + if(_use_stored) + _saturation.set_value(_saturation_store); + else + _saturation.set_from_attribute(o); + break; + case COLORMATRIX_HUEROTATE: + add(_angle); + if(_use_stored) + _angle.set_value(_angle_store); + else + _angle.set_from_attribute(o); + break; + case COLORMATRIX_LUMINANCETOALPHA: + add(_label); + break; + case COLORMATRIX_MATRIX: + default: + add(_matrix); + if(_use_stored) + _matrix.set_values(_matrix_store); + else + _matrix.set_from_attribute(o); + break; + } + _use_stored = true; + } + } + + Glib::ustring get_as_attribute() const override + { + const Widget* w = get_child(); + if(w == &_label) + return ""; + else + return dynamic_cast(w)->get_as_attribute(); + } + + void clear_store() + { + _use_stored = false; + } +private: + void update_store() + { + const Widget* w = get_child(); + if(w == &_matrix) + _matrix_store = _matrix.get_values(); + else if(w == &_saturation) + _saturation_store = _saturation.get_value(); + else if(w == &_angle) + _angle_store = _angle.get_value(); + } + + MatrixAttr _matrix; + SpinScale _saturation; + SpinScale _angle; + Gtk::Label _label; + + // Store separate values for the different color modes + bool _use_stored; + std::vector _matrix_store; + double _saturation_store; + double _angle_store; +}; + +static Inkscape::UI::Dialog::FileOpenDialog * selectFeImageFileInstance = nullptr; + +//Displays a chooser for feImage input +//It may be a filename or the id for an SVG Element +//described in xlink:href syntax +class FileOrElementChooser : public Gtk::HBox, public AttrWidget +{ +public: + FileOrElementChooser(const SPAttributeEnum a) + : AttrWidget(a) + { + pack_start(_entry, false, false); + pack_start(_fromFile, false, false); + pack_start(_fromSVGElement, false, false); + + _fromFile.set_label(_("Image File")); + _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file)); + + _fromSVGElement.set_label(_("Selected SVG Element")); + _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element)); + + _entry.signal_changed().connect(signal_attr_changed().make_slot()); + + show_all(); + + } + + // Returns the element in xlink:href form. + Glib::ustring get_as_attribute() const override + { + return _entry.get_text(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + _entry.set_text(val); + } else { + _entry.set_text(""); + } + } + + void set_desktop(SPDesktop* d){ + _desktop = d; + } + +private: + void select_svg_element(){ + Inkscape::Selection* sel = _desktop->getSelection(); + if (sel->isEmpty()) return; + Inkscape::XML::Node* node = sel->xmlNodes().front(); + if (!node || !node->matchAttributeName("id")) return; + + std::ostringstream xlikhref; + xlikhref << "#" << node->attribute("id"); + _entry.set_text(xlikhref.str()); + } + + void select_file(){ + + //# Get the current directory for finding files + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring open_path; + Glib::ustring attr = prefs->getString("/dialogs/open/path"); + if (!attr.empty()) + open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.size() < 1) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog if we don't already have one + if (!selectFeImageFileInstance) { + selectFeImageFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *_desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/ + (char const *)_("Select an image to be used as feImage input")); + } + + //# Show the dialog + bool const success = selectFeImageFileInstance->show(); + if (!success) + return; + + //# User selected something. Get name and type + Glib::ustring fileName = selectFeImageFileInstance->getFilename(); + + if (fileName.size() > 0) { + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + fileName = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + open_path = fileName; + open_path.append(G_DIR_SEPARATOR_S); + prefs->setString("/dialogs/open/path", open_path); + + _entry.set_text(fileName); + } + return; + } + + Gtk::Entry _entry; + Gtk::Button _fromFile; + Gtk::Button _fromSVGElement; + SPDesktop* _desktop; +}; + +class FilterEffectsDialog::Settings +{ +public: + typedef sigc::slot SetAttrSlot; + + Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes) + : _dialog(d), _set_attr_slot(std::move(slot)), _current_type(-1), _max_types(maxtypes) + { + _groups.resize(_max_types); + _attrwidgets.resize(_max_types); + _size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + + for(int i = 0; i < _max_types; ++i) { + _groups[i] = new Gtk::VBox(false, 3); + b.set_spacing(4); + b.pack_start(*_groups[i], Gtk::PACK_SHRINK); + } + //_current_type = 0; If set to 0 then update_and_show() fails to update properly. + } + + ~Settings() + { + for(int i = 0; i < _max_types; ++i) { + delete _groups[i]; + for(auto & j : _attrwidgets[i]) + delete j; + } + } + + // Show the active settings group and update all the AttrWidgets with new values + void show_and_update(const int t, SPObject* ob) + { + if(t != _current_type) { + type(t); + for(auto & _group : _groups) + _group->hide(); + } + if(t >= 0) { + _groups[t]->show(); // Do not use show_all(), it shows children than should be hidden + } + _dialog.set_attrs_locked(true); + for(auto & i : _attrwidgets[_current_type]) + i->set_from_attribute(ob); + _dialog.set_attrs_locked(false); + } + + int get_current_type() const + { + return _current_type; + } + + void type(const int t) + { + _current_type = t; + } + + void add_no_params() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect does not require any parameters."))); + add_widget(lbl, ""); + } + + void add_notimplemented() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect is not yet implemented in Inkscape."))); + add_widget(lbl, ""); + } + + // LightSource + LightSourceControl* add_lightsource(); + + // Component Transfer Values + ComponentTransferValues* add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel); + + // CheckButton + CheckButtonAttr* add_checkbutton(bool def, const SPAttributeEnum attr, const Glib::ustring& label, + const Glib::ustring& tv, const Glib::ustring& fv, char* tip_text = nullptr) + { + CheckButtonAttr* cb = new CheckButtonAttr(def, label, tv, fv, attr, tip_text); + add_widget(cb, ""); + add_attr_widget(cb); + return cb; + } + + // ColorButton + ColorButton* add_color(unsigned int def, const SPAttributeEnum attr, const Glib::ustring& label, char* tip_text = nullptr) + { + ColorButton* col = new ColorButton(def, attr, tip_text); + add_widget(col, label); + add_attr_widget(col); + return col; + } + + // Matrix + MatrixAttr* add_matrix(const SPAttributeEnum attr, const Glib::ustring& label, char* tip_text) + { + MatrixAttr* conv = new MatrixAttr(attr, tip_text); + add_widget(conv, label); + add_attr_widget(conv); + return conv; + } + + // ColorMatrixValues + ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label) + { + ColorMatrixValues* cmv = new ColorMatrixValues(); + add_widget(cmv, label); + add_attr_widget(cmv); + return cmv; + } + + // SpinScale + SpinScale* add_spinscale(double def, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, const double climb, const int digits, char* tip_text = nullptr) + { + Glib::ustring tip_text2; + if (tip_text) + tip_text2 = tip_text; + SpinScale* spinslider = new SpinScale("", def, lo, hi, step_inc, climb, digits, attr, tip_text2); + add_widget(spinslider, label); + add_attr_widget(spinslider); + return spinslider; + } + + // DualSpinScale + DualSpinScale* add_dualspinscale(const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, + const Glib::ustring tip_text1 = "", + const Glib::ustring tip_text2 = "") + { + DualSpinScale* dss = new DualSpinScale("", "", lo, lo, hi, step_inc, climb, digits, attr, tip_text1, tip_text2); + add_widget(dss, label); + add_attr_widget(dss); + return dss; + } + + // SpinButton + SpinButtonAttr* add_spinbutton(double defalt_value, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip = nullptr) + { + SpinButtonAttr* sb = new SpinButtonAttr(lo, hi, step_inc, climb, digits, attr, defalt_value, tip); + add_widget(sb, label); + add_attr_widget(sb); + return sb; + } + + // DualSpinButton + DualSpinButton* add_dualspinbutton(char* defalt_value, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + DualSpinButton* dsb = new DualSpinButton(defalt_value, lo, hi, step_inc, climb, digits, attr, tip1, tip2); + add_widget(dsb, label); + add_attr_widget(dsb); + return dsb; + } + + // MultiSpinButton + MultiSpinButton* add_multispinbutton(double def1, double def2, const SPAttributeEnum attr1, const SPAttributeEnum attr2, + const Glib::ustring& label, const double lo, const double hi, + const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + std::vector attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + + std::vector default_values; + default_values.push_back(def1); + default_values.push_back(def2); + + std::vector tips; + tips.push_back(tip1); + tips.push_back(tip2); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + MultiSpinButton* add_multispinbutton(double def1, double def2, double def3, const SPAttributeEnum attr1, const SPAttributeEnum attr2, + const SPAttributeEnum attr3, const Glib::ustring& label, const double lo, + const double hi, const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr, char* tip3 = nullptr) + { + std::vector attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + attrs.push_back(attr3); + + std::vector default_values; + default_values.push_back(def1); + default_values.push_back(def2); + default_values.push_back(def3); + + std::vector tips; + tips.push_back(tip1); + tips.push_back(tip2); + tips.push_back(tip3); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + + // FileOrElementChooser + FileOrElementChooser* add_fileorelement(const SPAttributeEnum attr, const Glib::ustring& label) + { + FileOrElementChooser* foech = new FileOrElementChooser(attr); + foech->set_desktop(_dialog.getDesktop()); + add_widget(foech, label); + add_attr_widget(foech); + return foech; + } + + // ComboBoxEnum + template ComboBoxEnum* add_combo(T default_value, const SPAttributeEnum attr, + const Glib::ustring& label, + const Util::EnumDataConverter& conv, char* tip_text = nullptr) + { + ComboWithTooltip* combo = new ComboWithTooltip(default_value, conv, attr, tip_text); + add_widget(combo, label); + add_attr_widget(combo->get_attrwidget()); + return combo->get_attrwidget(); + } + + // Entry + EntryAttr* add_entry(const SPAttributeEnum attr, + const Glib::ustring& label, + char* tip_text = nullptr) + { + EntryAttr* entry = new EntryAttr(attr, tip_text); + add_widget(entry, label); + add_attr_widget(entry); + return entry; + } + + Glib::RefPtr _size_group; +private: + void add_attr_widget(AttrWidget* a) + { + _attrwidgets[_current_type].push_back(a); + a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a)); + } + + /* Adds a new settings widget using the specified label. The label will be formatted with a colon + and all widgets within the setting group are aligned automatically. */ + void add_widget(Gtk::Widget* w, const Glib::ustring& label) + { + Gtk::HBox *hb = Gtk::manage(new Gtk::HBox); + hb->set_spacing(12); + + if (label != "") { + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(label)); + lbl->set_xalign(0.0); + hb->pack_start(*lbl, Gtk::PACK_SHRINK); + _size_group->add_widget(*lbl); + } + + hb->pack_start(*w, Gtk::PACK_EXPAND_WIDGET); + _groups[_current_type]->pack_start(*hb, Gtk::PACK_EXPAND_WIDGET); + hb->show_all(); + } + + std::vector _groups; + FilterEffectsDialog& _dialog; + SetAttrSlot _set_attr_slot; + std::vector > _attrwidgets; + int _current_type, _max_types; +}; + +// Displays sliders and/or tables for feComponentTransfer +class FilterEffectsDialog::ComponentTransferValues : public Gtk::Frame, public AttrWidget +{ +public: + ComponentTransferValues(FilterEffectsDialog& d, SPFeFuncNode::Channel channel) + : AttrWidget(SP_ATTR_INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR), + _type(ComponentTransferTypeConverter, SP_ATTR_TYPE, false), + _channel(channel), + _funcNode(nullptr) + { + set_shadow_type(Gtk::SHADOW_IN); + add(_box); + _box.add(_type); + _box.reorder_child(_type, 0); + _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed)); + + _settings.type(COMPONENTTRANSFER_TYPE_LINEAR); + _settings.add_spinscale(1, SP_ATTR_SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_GAMMA); + _settings.add_spinscale(1, SP_ATTR_AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(1, SP_ATTR_EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_TABLE); + _settings.add_entry(SP_ATTR_TABLEVALUES, _("Table")); + + _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE); + _settings.add_entry(SP_ATTR_TABLEVALUES, _("Discrete")); + + //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY); + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + } + + // FuncNode can be in any order so we must search to find correct one. + SPFeFuncNode* find_node(SPFeComponentTransfer* ct) + { + SPFeFuncNode* funcNode = nullptr; + bool found = false; + for(auto& node: ct->children) { + funcNode = SP_FEFUNCNODE(&node); + if( funcNode->channel == _channel ) { + found = true; + break; + } + } + if( !found ) + funcNode = nullptr; + + return funcNode; + } + + void set_func_attr(const AttrWidget* input) + { + _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str()); + } + + // Set new type and update widget visibility + void set_from_attribute(SPObject* o) override + { + // See componenttransfer.cpp + if(SP_IS_FECOMPONENTTRANSFER(o)) { + SPFeComponentTransfer* ct = SP_FECOMPONENTTRANSFER(o); + + _funcNode = find_node(ct); + if( _funcNode ) { + _type.set_from_attribute( _funcNode ); + } else { + // Create + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = nullptr; + switch(_channel) { + case SPFeFuncNode::R: + repr = xml_doc->createElement("svg:feFuncR"); + break; + case SPFeFuncNode::G: + repr = xml_doc->createElement("svg:feFuncG"); + break; + case SPFeFuncNode::B: + repr = xml_doc->createElement("svg:feFuncB"); + break; + case SPFeFuncNode::A: + repr = xml_doc->createElement("svg:feFuncA"); + break; + } + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Now we should find it! + _funcNode = find_node(ct); + if( _funcNode ) { + _funcNode->setAttribute( "type", "identity" ); + } else { + //std::cout << "ERROR ERROR: feFuncX not found!" << std::endl; + } + } + } + + update(); + } + } + +private: + void on_type_changed() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + + _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute()); + + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("New transfer function type")); + update(); + } + } + + void update() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && _funcNode) { + _settings.show_and_update(_type.get_active_data()->id, _funcNode); + } + } + +public: + Glib::ustring get_as_attribute() const override + { + return ""; + } + + FilterEffectsDialog& _dialog; + Gtk::VBox _box; + Settings _settings; + ComboBoxEnum _type; + SPFeFuncNode::Channel _channel; // RGBA + SPFeFuncNode* _funcNode; +}; + +// Settings for the three light source objects +class FilterEffectsDialog::LightSourceControl : public AttrWidget +{ +public: + LightSourceControl(FilterEffectsDialog& d) + : AttrWidget(SP_ATTR_INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE), + _light_label(_("Light Source:")), + _light_source(LightSourceConverter), + _locked(false) + { + _light_label.set_xalign(0.0); + _settings._size_group->add_widget(_light_label); + _light_box.pack_start(_light_label, Gtk::PACK_SHRINK); + _light_box.pack_start(_light_source, Gtk::PACK_EXPAND_WIDGET); + _light_box.show_all(); + _light_box.set_spacing(12); + + _box.add(_light_box); + _box.reorder_child(_light_box, 0); + _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed)); + + // FIXME: these range values are complete crap + + _settings.type(LIGHT_DISTANT); + _settings.add_spinscale(0, SP_ATTR_AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees")); + _settings.add_spinscale(0, SP_ATTR_ELEVATION, _("Elevation:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the YZ plane, in degrees")); + + _settings.type(LIGHT_POINT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + + _settings.type(LIGHT_SPOT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, + SP_ATTR_POINTSATX, SP_ATTR_POINTSATY, SP_ATTR_POINTSATZ, + _("Points At:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_spinscale(1, SP_ATTR_SPECULAREXPONENT, _("Specular Exponent:"), 1, 100, 1, 1, 0, _("Exponent value controlling the focus for the light source")); + //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option. + _settings.add_spinscale(100, SP_ATTR_LIMITINGCONEANGLE, _("Cone Angle:"), 1, 100, 1, 1, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone.")); + + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + + } + + Gtk::VBox& get_box() + { + return _box; + } +protected: + Glib::ustring get_as_attribute() const override + { + return ""; + } + void set_from_attribute(SPObject* o) override + { + if(_locked) + return; + + _locked = true; + + SPObject* child = o->firstChild(); + + if(SP_IS_FEDISTANTLIGHT(child)) + _light_source.set_active(0); + else if(SP_IS_FEPOINTLIGHT(child)) + _light_source.set_active(1); + else if(SP_IS_FESPOTLIGHT(child)) + _light_source.set_active(2); + else + _light_source.set_active(-1); + + update(); + + _locked = false; + } +private: + void on_source_changed() + { + if(_locked) + return; + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + _locked = true; + + SPObject* child = prim->firstChild(); + const int ls = _light_source.get_active_row_number(); + // Check if the light source type has changed + if(!(ls == -1 && !child) && + !(ls == 0 && SP_IS_FEDISTANTLIGHT(child)) && + !(ls == 1 && SP_IS_FEPOINTLIGHT(child)) && + !(ls == 2 && SP_IS_FESPOTLIGHT(child))) { + if(child) + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(child->getRepr()); + + if(ls != -1) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str()); + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + } + + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("New light source")); + update(); + } + + _locked = false; + } + } + + void update() + { + _box.hide(); + _box.show(); + _light_box.show_all(); + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && prim->firstChild()) + _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild()); + } + + FilterEffectsDialog& _dialog; + Gtk::VBox _box; + Settings _settings; + Gtk::HBox _light_box; + Gtk::Label _light_label; + ComboBoxEnum _light_source; + bool _locked; +}; + + // ComponentTransferValues +FilterEffectsDialog::ComponentTransferValues* FilterEffectsDialog::Settings::add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel) + { + ComponentTransferValues* ct = new ComponentTransferValues(_dialog, channel); + add_widget(ct, label); + add_attr_widget(ct); + return ct; + } + + +FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource() +{ + LightSourceControl* ls = new LightSourceControl(_dialog); + add_attr_widget(ls); + add_widget(&ls->get_box(), ""); + return ls; +} + +static Gtk::Menu * create_popup_menu(Gtk::Widget& parent, + sigc::slot dup, + sigc::slot rem) +{ + auto menu = Gtk::manage(new Gtk::Menu); + + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Duplicate"),true)); + mi->signal_activate().connect(dup); + mi->show(); + menu->append(*mi); + + mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + menu->append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + menu->accelerate(parent); + + return menu; +} + +/*** FilterModifier ***/ +FilterEffectsDialog::FilterModifier::FilterModifier(FilterEffectsDialog& d) + : _desktop(nullptr), + _deskTrack(), + _dialog(d), + _add(_("_New"), true), + _observer(new Inkscape::XML::SignalObserver) +{ + Gtk::ScrolledWindow* sw = Gtk::manage(new Gtk::ScrolledWindow); + pack_start(*sw); + pack_start(_add, false, false); + sw->add(_list); + + _model = Gtk::ListStore::create(_columns); + _list.set_model(_model); + _cell_toggle.set_active(true); + const int selcol = _list.append_column("", _cell_toggle); + Gtk::TreeViewColumn* col = _list.get_column(selcol - 1); + if(col) + col->add_attribute(_cell_toggle.property_active(), _columns.sel); + _list.append_column_editable(_("_Filter"), _columns.label); + ((Gtk::CellRendererText*)_list.get_column(1)->get_first_cell())-> + signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited)); + + _list.append_column("#", _columns.count); + _list.get_column(2)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(2)->set_expand(false); + _list.get_column(2)->set_reorderable(true); + + sw->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _list.get_column(1)->set_resizable(true); + _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(1)->set_expand(true); + + _list.set_reorderable(true); + _list.enable_model_drag_dest (Gdk::ACTION_MOVE); + + _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false ); + + sw->set_shadow_type(Gtk::SHADOW_IN); + show_all_children(); + _add.signal_clicked().connect(sigc::mem_fun(*this, &FilterModifier::add_filter)); + _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled)); + _list.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &FilterModifier::filter_list_button_release)); + + _menu = create_popup_menu(*this, + sigc::mem_fun(*this, &FilterModifier::duplicate_filter), + sigc::mem_fun(*this, &FilterModifier::remove_filter)); + + Gtk::MenuItem *item = Gtk::manage(new Gtk::MenuItem(_("R_ename"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::rename_filter)); + item->show(); + _menu->append(*item); + _menu->accelerate(*this); + + _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed)); + _observer->signal_changed().connect(signal_filter_changed().make_slot()); + + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FilterModifier::setTargetDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + update_filters(); +} + +FilterEffectsDialog::FilterModifier::~FilterModifier() +{ + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _resource_changed.disconnect(); + _doc_replaced.disconnect(); +} + +void FilterEffectsDialog::FilterModifier::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + if (_desktop) { + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _doc_replaced.disconnect(); + _resource_changed.disconnect(); + _dialog.setDesktop(nullptr); + } + _desktop = desktop; + if (desktop) { + if (desktop->selection) { + _selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FilterModifier::on_change_selection))); + _selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FilterModifier::on_modified_selection))); + } + _doc_replaced = desktop->connectDocumentReplaced( sigc::mem_fun(*this, &FilterModifier::on_document_replaced)); + _resource_changed = desktop->getDocument()->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + _dialog.setDesktop(desktop); + + update_filters(); + } + } +} + +// When the document changes, update connection to resources +void FilterEffectsDialog::FilterModifier::on_document_replaced(SPDesktop * /*desktop*/, SPDocument *document) +{ + if (_resource_changed) { + _resource_changed.disconnect(); + } + if (document) + { + _resource_changed = document->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + } + + update_filters(); +} + +// When the selection changes, show the active filter(s) in the dialog +void FilterEffectsDialog::FilterModifier::on_change_selection() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + update_selection(selection); +} + +void FilterEffectsDialog::FilterModifier::on_modified_selection( guint flags ) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + on_change_selection(); + } +} + +// Update each filter's sel property based on the current object selection; +// If the filter is not used by any selected object, sel = 0, +// otherwise sel is set to the total number of filters in use by selected objects +// If only one filter is in use, it is selected +void FilterEffectsDialog::FilterModifier::update_selection(Selection *sel) +{ + if (!sel) { + return; + } + + std::set used; + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPObject *obj = *i; + SPStyle *style = obj->style; + if (!style || !SP_IS_ITEM(obj)) { + continue; + } + + if (style->filter.set && style->getFilter()) { + SP_ITEM(obj)->bbox_valid = FALSE; + used.insert(style->getFilter()); + } else { + used.insert(nullptr); + } + } + + const int size = used.size(); + + for (Gtk::TreeIter iter = _model->children().begin(); iter != _model->children().end(); ++iter) { + if (used.find((*iter)[_columns.filter]) != used.end()) { + // If only one filter is in use by the selection, select it + if (size == 1) { + _list.get_selection()->select(iter); + } + (*iter)[_columns.sel] = size; + } else { + (*iter)[_columns.sel] = 0; + } + } + update_counts(); +} + +void FilterEffectsDialog::FilterModifier::on_filter_selection_changed() +{ + _observer->set(get_selected_filter()); + signal_filter_changed()(); +} + +void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text) +{ + Gtk::TreeModel::iterator iter = _model->get_iter(path); + + if(iter) { + SPFilter* filter = (*iter)[_columns.filter]; + filter->setLabel(text.c_str()); + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Rename filter")); + if(iter) + (*iter)[_columns.label] = text; + } +} + +bool FilterEffectsDialog::FilterModifier::on_filter_move(const Glib::RefPtr& /*context*/, int /*x*/, int /*y*/, guint /*time*/) { + +//const Gtk::TreeModel::Path& /*path*/) { +/* The code below is bugged. Use of "object->getRepr()->setPosition(0)" is dangerous! + Writing back the reordered list to XML (reordering XML nodes) should be implemented differently. + Note that the dialog does also not update its list of filters when the order is manually changed + using the XML dialog + for(Gtk::TreeModel::iterator i = _model->children().begin(); i != _model->children().end(); ++i) { + SPObject* object = (*i)[_columns.filter]; + if(object && object->getRepr()) ; + object->getRepr()->setPosition(0); + } +*/ + return false; +} + +void FilterEffectsDialog::FilterModifier::on_selection_toggled(const Glib::ustring& path) +{ + Gtk::TreeIter iter = _model->get_iter(path); + + if(iter) { + SPDesktop *desktop = _dialog.getDesktop(); + SPDocument *doc = desktop->getDocument(); + SPFilter* filter = (*iter)[_columns.filter]; + Inkscape::Selection *sel = desktop->getSelection(); + + /* If this filter is the only one used in the selection, unset it */ + if((*iter)[_columns.sel] == 1) + filter = nullptr; + + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPItem * item = *i; + SPStyle *style = item->style; + g_assert(style != nullptr); + + if (filter) { + sp_style_set_property_url(item, "filter", filter, false); + } else { + ::remove_filter(item, false); + } + + item->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG )); + } + + update_selection(sel); + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Apply filter")); + } +} + + +void FilterEffectsDialog::FilterModifier::update_counts() +{ + for(const auto & i : _model->children()) { + SPFilter* f = SP_FILTER(i[_columns.filter]); + i[_columns.count] = f->getRefCount(); + } +} + +/* Add all filters in the document to the combobox. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::FilterModifier::update_filters() +{ + SPDesktop* desktop = _dialog.getDesktop(); + SPDocument* document = desktop->getDocument(); + + // Workaround for 1.0, not needed in 1.1 (which properly disconnects signals) + if (!document) { + return; + } + + std::vector filters = document->getResourceList( "filter" ); + + _model->clear(); + + for (auto filter : filters) { + Gtk::TreeModel::Row row = *_model->append(); + SPFilter* f = SP_FILTER(filter); + row[_columns.filter] = f; + const gchar* lbl = f->label(); + const gchar* id = f->getId(); + row[_columns.label] = lbl ? lbl : (id ? id : "filter"); + } + + update_selection(desktop->selection); + _dialog.update_filter_general_settings_view(); +} + +SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter() +{ + if(_list.get_selection()) { + Gtk::TreeModel::iterator i = _list.get_selection()->get_selected(); + + if(i) + return (*i)[_columns.filter]; + } + + return nullptr; +} + +void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter) +{ + if(filter) { + for(Gtk::TreeModel::iterator i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.filter] == filter) { + _list.get_selection()->select(i); + break; + } + } + } +} + +void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + const bool sensitive = get_selected_filter() != nullptr; + auto items = _menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + + _menu->popup_at_pointer(reinterpret_cast(event)); + } +} + +void FilterEffectsDialog::FilterModifier::add_filter() +{ + SPDocument* doc = _dialog.getDesktop()->getDocument(); + SPFilter* filter = new_filter(doc); + + const int count = _model->children().size(); + std::ostringstream os; + os << _("filter") << count; + filter->setLabel(os.str().c_str()); + + update_filters(); + + select_filter(filter); + + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter")); +} + +void FilterEffectsDialog::FilterModifier::remove_filter() +{ + SPFilter *filter = get_selected_filter(); + + if(filter) { + SPDocument* doc = filter->document; + + // Delete all references to this filter + std::vector x,y; + std::vector all = get_all_items(x, _desktop->currentRoot(), _desktop, false, false, true, y); + for(std::vector::const_iterator i=all.begin(); all.end() != i; ++i) { + if (!SP_IS_ITEM(*i)) { + continue; + } + SPItem *item = *i; + if (!item->style) { + continue; + } + + const SPIFilter *ifilter = &(item->style->filter); + if (ifilter && ifilter->href) { + const SPObject *obj = ifilter->href->getObject(); + if (obj && obj == (SPObject *)filter) { + ::remove_filter(item, false); + } + } + } + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(filter->getRepr()); + + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Remove filter")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::duplicate_filter() +{ + SPFilter* filter = get_selected_filter(); + + if (filter) { + Inkscape::XML::Node *repr = filter->getRepr(); + Inkscape::XML::Node *parent = repr->parent(); + repr = repr->duplicate(repr->document()); + parent->appendChild(repr); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::rename_filter() +{ + _list.set_cursor(_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true); +} + +FilterEffectsDialog::CellRendererConnection::CellRendererConnection() + : Glib::ObjectBase(typeid(CellRendererConnection)), + _primitive(*this, "primitive", nullptr), + _text_width(0) +{} + +Glib::PropertyProxy FilterEffectsDialog::CellRendererConnection::property_primitive() +{ + return _primitive.get_proxy(); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const +{ + PrimitiveList& primlist = dynamic_cast(widget); + minimum_width = natural_width = size * primlist.primitive_count() + primlist.get_input_type_width() * 6; +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int /* height */, + int& minimum_width, + int& natural_width) const +{ + get_preferred_width(widget, minimum_width, natural_width); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const +{ + // Scale the height depending on the number of inputs, unless it's + // the first primitive, in which case there are no connections + SPFilterPrimitive* prim = SP_FILTER_PRIMITIVE(_primitive.get_value()); + minimum_height = natural_height = size * input_count(prim); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int /* width */, + int& minimum_height, + int& natural_height) const +{ + get_preferred_height(widget, minimum_height, natural_height); +} + +/*** PrimitiveList ***/ +FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d) + : _dialog(d), + _in_drag(0), + _observer(new Inkscape::XML::SignalObserver) +{ + signal_draw().connect(sigc::mem_fun(*this, &PrimitiveList::on_draw_signal)); + + add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); + + _model = Gtk::ListStore::create(_columns); + + set_reorderable(true); + + set_model(_model); + append_column(_("_Effect"), _columns.type); + get_column(0)->set_resizable(true); + set_headers_visible(); + + _observer->signal_changed().connect(signal_primitive_changed().make_slot()); + get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed)); + signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw)); + + init_text(); + + int cols_count = append_column(_("Connections"), _connection_cell); + Gtk::TreeViewColumn* col = get_column(cols_count - 1); + if(col) + col->add_attribute(_connection_cell.property_primitive(), _columns.primitive); +} + +// Sets up a vertical Pango context/layout, and returns the largest +// width needed to render the FilterPrimitiveInput labels. +void FilterEffectsDialog::PrimitiveList::init_text() +{ + // Set up a vertical context+layout + Glib::RefPtr context = create_pango_context(); + const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0}; + context->set_matrix(matrix); + _vertical_layout = Pango::Layout::create(context); + + // Store the maximum height and width of the an input type label + // for later use in drawing and measuring. + _input_type_height = _input_type_width = 0; + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + int fontw, fonth; + _vertical_layout->get_pixel_size(fontw, fonth); + if(fonth > _input_type_width) + _input_type_width = fonth; + if (fontw > _input_type_height) + _input_type_height = fontw; + } +} + +sigc::signal& FilterEffectsDialog::PrimitiveList::signal_primitive_changed() +{ + return _signal_primitive_changed; +} + +void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed() +{ + _observer->set(get_selected()); + signal_primitive_changed()(); + _dialog._color_matrix_values->clear_store(); +} + +/* Add all filter primitives in the current to the list. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::PrimitiveList::update() +{ + SPFilter* f = _dialog._filter_modifier.get_selected_filter(); + const SPFilterPrimitive* active_prim = get_selected(); + _model->clear(); + + if(f) { + bool active_found = false; + _dialog._primitive_box->set_sensitive(true); + _dialog.update_filter_general_settings_view(); + for(auto& prim_obj: f->children) { + SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(&prim_obj); + if(!prim) { + break; + } + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.primitive] = prim; + + //XML Tree being used directly here while it shouldn't be. + row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name()); + row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str()); + + if (prim->getId()) { + row[_columns.id] = Glib::ustring(prim->getId()); + } + + if(prim == active_prim) { + get_selection()->select(row); + active_found = true; + } + } + + if(!active_found && _model->children().begin()) + get_selection()->select(_model->children().begin()); + + columns_autosize(); + + int width, height; + get_size_request(width, height); + if (height == -1) { + // Need to account for the height of the input type text (rotated text) as well as the + // column headers. Input type text height determined in init_text() by measuring longest + // string. Column header height determined by mapping y coordinate of visible + // rectangle to widget coordinates. + Gdk::Rectangle vis; + int vis_x, vis_y; + get_visible_rect(vis); + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + set_size_request(width, _input_type_height + 2 + vis_y); + } + } + else { + _dialog._primitive_box->set_sensitive(false); + set_size_request(-1, -1); + } +} + +void FilterEffectsDialog::PrimitiveList::set_menu(Gtk::Widget& parent, + sigc::slot dup, + sigc::slot rem) +{ + _primitive_menu = create_popup_menu(parent, dup, rem); +} + +SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected() +{ + if(_dialog._filter_modifier.get_selected_filter()) { + Gtk::TreeModel::iterator i = get_selection()->get_selected(); + if(i) + return (*i)[_columns.primitive]; + } + + return nullptr; +} + +void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim) +{ + for(Gtk::TreeIter i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.primitive] == prim) + get_selection()->select(i); + } +} + +void FilterEffectsDialog::PrimitiveList::remove_selected() +{ + SPFilterPrimitive* prim = get_selected(); + + if(prim) { + _observer->set(nullptr); + _model->erase(get_selection()->get_selected()); + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(prim->getRepr()); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove filter primitive")); + + update(); + } +} + +bool FilterEffectsDialog::PrimitiveList::on_draw_signal(const Cairo::RefPtr & cr) +{ + cr->set_line_width(1.0); + // In GTK+ 3, the draw function receives the widget window, not the + // bin_window (i.e., just the area under the column headers). We + // therefore translate the origin of our coordinate system to account for this + int x_origin, y_origin; + convert_bin_window_to_widget_coords(0,0,x_origin,y_origin); + cr->translate(x_origin, y_origin); + + auto sc = gtk_widget_get_style_context(GTK_WIDGET(gobj())); + GdkRGBA bg_color, fg_color; + gtk_style_context_get_color(sc, GTK_STATE_FLAG_NORMAL, &fg_color); + gtk_style_context_get_background_color(sc, GTK_STATE_FLAG_NORMAL, &bg_color); + GdkRGBA orig_color {fg_color}; + + auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; }; + fg_color.red = lerp(bg_color.red, orig_color.red, 0.95); + fg_color.green = lerp(bg_color.green, orig_color.green, 0.95); + fg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.95); + bg_color.red = lerp(bg_color.red, orig_color.red, 0.05); + bg_color.green = lerp(bg_color.green, orig_color.green, 0.05); + bg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.05); + GdkRGBA mid_color { + lerp(bg_color.red, fg_color.red, 0.65), + lerp(bg_color.green, fg_color.green, 0.65), + lerp(bg_color.blue, fg_color.blue, 0.65), + fg_color.alpha}; + + SPFilterPrimitive* prim = get_selected(); + int row_count = get_model()->children().size(); + + int fheight = CellRendererConnection::size; + Gdk::Rectangle rct, vis; + Gtk::TreeIter row = get_model()->children().begin(); + int text_start_x = 0; + if(row) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + get_visible_rect(vis); + text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * FPInputConverter._length + 1; + + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + const int x = text_start_x + get_input_type_width() * i; + cr->save(); + gdk_cairo_set_source_rgba(cr->cobj(), &bg_color); + cr->rectangle(x, 0, get_input_type_width(), vis.get_height()); + cr->fill_preserve(); + + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + cr->move_to(x + get_input_type_width(), 5); + cr->rotate_degrees(90); + _vertical_layout->show_in_cairo_context(cr); + + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + cr->move_to(x, 0); + cr->line_to(x, vis.get_height()); + cr->stroke(); + cr->restore(); + } + cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height()); + cairo_clip(cr->cobj()); + } + + int row_index = 0; + for(; row != get_model()->children().end(); ++row, ++row_index) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height(); + + // Check mouse state + int mx, my; + Gdk::ModifierType mask; + + auto display = get_bin_window()->get_display(); + auto seat = display->get_default_seat(); + auto device = seat->get_pointer(); + cairo_set_line_width (cr->cobj(),0.5); + get_bin_window()->get_device_position(device, mx, my, mask); + + // Outline the bottom of the connection area + const int outline_x = x + fheight * (row_count - row_index); + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + + cr->move_to(vis.get_x(), y + h); + cr->line_to(outline_x, y + h); + // Side outline + cr->line_to(outline_x, y - 1); + + cr->stroke(); + cr->restore(); + + std::vector con_poly; + int con_drag_y = 0; + int con_drag_x = 0; + bool inside; + const SPFilterPrimitive* row_prim = (*row)[_columns.primitive]; + const int inputs = input_count(row_prim); + + if(SP_IS_FEMERGE(row_prim)) { + for(int i = 0; i < inputs; ++i) { + inside = do_connection_node(row, i, con_poly, mx, my); + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + if(_in_drag == (i + 1)) + { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + if(_in_drag != (i + 1) || row_prim != prim) + { + draw_connection(cr, row, i, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + } + } + else { + // Draw "in" shape + inside = do_connection_node(row, 0, con_poly, mx, my); + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in" connection + if(_in_drag != 1 || row_prim != prim) + { + draw_connection(cr, row, SP_ATTR_IN, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + + if(inputs == 2) { + // Draw "in2" shape + inside = do_connection_node(row, 1, con_poly, mx, my); + if(_in_drag == 2) + { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in2" connection + if(_in_drag != 2 || row_prim != prim) + { + draw_connection(cr, row, SP_ATTR_IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + } + } + + // Draw drag connection + if(row_prim == prim && _in_drag) { + cr->save(); + gdk_cairo_set_source_rgba(cr->cobj(), &orig_color); + cr->move_to(con_drag_x, con_drag_y); + cr->line_to(mx, con_drag_y); + cr->line_to(mx, my); + cr->stroke(); + cr->restore(); + } + } + + return true; +} + +void FilterEffectsDialog::PrimitiveList::draw_connection(const Cairo::RefPtr& cr, + const Gtk::TreeIter& input, const int attr, + const int text_start_x, const int x1, const int y1, + const int row_count, const GdkRGBA fg_color, const GdkRGBA mid_color) +{ + cr->save(); + + int src_id = 0; + Gtk::TreeIter res = find_result(input, attr, src_id); + + const bool is_first = input == get_model()->children().begin(); + const bool is_merge = SP_IS_FEMERGE((SPFilterPrimitive*)(*input)[_columns.primitive]); + const bool use_default = !res && !is_merge; + + if(res == input || (use_default && is_first)) { + // Draw straight connection to a standard input + // Draw a lighter line for an implicit connection to a standard input + const int tw = get_input_type_width(); + gint end_x = text_start_x + tw * src_id + (int)(tw * 0.5f) + 1; + + if(use_default && is_first) + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + else + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + + cr->rectangle(end_x-2, y1-2, 5, 5); + cr->fill_preserve(); + cr->move_to(x1, y1); + cr->line_to(end_x, y1); + cr->stroke(); + } + else { + // Draw an 'L'-shaped connection to another filter primitive + // If no connection is specified, draw a light connection to the previous primitive + if(use_default) { + res = input; + --res; + } + + if(res) { + Gdk::Rectangle rct; + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(get_model()->get_path(res), *get_column(1), rct); + const int row_index = find_index(res); + const int x2 = rct.get_x() + fheight * (row_count - row_index) - fheight / 2; + const int y2 = rct.get_y() + rct.get_height(); + + // Draw a bevelled 'L'-shaped connection + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + cr->move_to(x1, y1); + cr->line_to(x2-fheight/4, y1); + cr->line_to(x2, y1-fheight/4); + cr->line_to(x2, y2); + cr->stroke(); + } + } + cr->restore(); +} + +// Draw the triangular outline of the connection node, and fill it +// if desired +void FilterEffectsDialog::PrimitiveList::draw_connection_node(const Cairo::RefPtr& cr, + const std::vector& points, + const bool fill) +{ + cr->save(); + cr->move_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + cr->line_to(points[1].get_x()+0.5, points[1].get_y()+0.5); + cr->line_to(points[2].get_x()+0.5, points[2].get_y()+0.5); + cr->line_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + + if(fill) cr->fill(); + else cr->stroke(); + + cr->restore(); +} + +// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node +bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input, + std::vector& points, + const int ix, const int iy) +{ + Gdk::Rectangle rct; + const int icnt = input_count((*row)[_columns.primitive]); + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(_model->get_path(row), *get_column(1), rct); + const float h = rct.get_height() / icnt; + + const int x = rct.get_x() + fheight * (_model->children().size() - find_index(row)); + const int con_w = (int)(fheight * 0.35f); + const int con_y = (int)(rct.get_y() + (h / 2) - con_w + (input * h)); + points.clear(); + points.emplace_back(x, con_y); + points.emplace_back(x, con_y + con_w * 2); + points.emplace_back(x - con_w, con_y + con_w); + + return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y(); +} + +const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start, + const int attr, int& src_id) +{ + SPFilterPrimitive* prim = (*start)[_columns.primitive]; + Gtk::TreeIter target = _model->children().end(); + int image = 0; + + if(SP_IS_FEMERGE(prim)) { + int c = 0; + bool found = false; + for (auto& o: prim->children) { + if(c == attr && SP_IS_FEMERGENODE(&o)) { + image = SP_FEMERGENODE(&o)->input; + found = true; + } + ++c; + } + if(!found) + return target; + } + else { + if(attr == SP_ATTR_IN) + image = prim->image_in; + else if(attr == SP_ATTR_IN2) { + if(SP_IS_FEBLEND(prim)) + image = SP_FEBLEND(prim)->in2; + else if(SP_IS_FECOMPOSITE(prim)) + image = SP_FECOMPOSITE(prim)->in2; + else if(SP_IS_FEDISPLACEMENTMAP(prim)) + image = SP_FEDISPLACEMENTMAP(prim)->in2; + else + return target; + } + else + return target; + } + + if(image >= 0) { + for(Gtk::TreeIter i = _model->children().begin(); + i != start; ++i) { + if(((SPFilterPrimitive*)(*i)[_columns.primitive])->image_out == image) + target = i; + } + return target; + } + else if(image < -1) { + src_id = -(image + 2); + return start; + } + + return target; +} + +int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target) +{ + int i = 0; + for(Gtk::TreeIter iter = _model->children().begin(); + iter != target; ++iter, ++i){}; + return i; +} + +bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e) +{ + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + const int x = (int)e->x, y = (int)e->y; + int cx, cy; + + _drag_prim = nullptr; + + if(get_path_at_pos(x, y, path, col, cx, cy)) { + Gtk::TreeIter iter = _model->get_iter(path); + std::vector points; + + _drag_prim = (*iter)[_columns.primitive]; + const int icnt = input_count(_drag_prim); + + for(int i = 0; i < icnt; ++i) { + if(do_connection_node(_model->get_iter(path), i, points, x, y)) { + _in_drag = i + 1; + break; + } + } + + queue_draw(); + } + + if(_in_drag) { + _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150); + _autoscroll_x = 0; + _autoscroll_y = 0; + get_selection()->select(path); + return true; + } + else + return Gtk::TreeView::on_button_press_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e) +{ + const int speed = 10; + const int limit = 15; + + Gdk::Rectangle vis; + get_visible_rect(vis); + int vis_x, vis_y; + + int vis_x2, vis_y2; + convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2); + + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + const int top = vis_y + vis.get_height(); + const int right_edge = vis_x + vis.get_width(); + + // When autoscrolling during a connection drag, set the speed based on + // where the mouse is in relation to the edges. + if(e->y < vis_y) + _autoscroll_y = -(int)(speed + (vis_y - e->y) / 5); + else if(e->y < vis_y + limit) + _autoscroll_y = -speed; + else if(e->y > top) + _autoscroll_y = (int)(speed + (e->y - top) / 5); + else if(e->y > top - limit) + _autoscroll_y = speed; + else + _autoscroll_y = 0; + + double e2 = ( e->x - vis_x2/2); + // horizontal scrolling + if(e2 < vis_x) + _autoscroll_x = -(int)(speed + (vis_x - e2) / 5); + else if(e2 < vis_x + limit) + _autoscroll_x = -speed; + else if(e2 > right_edge) + _autoscroll_x = (int)(speed + (e2 - right_edge) / 5); + else if(e2 > right_edge - limit) + _autoscroll_x = speed; + else + _autoscroll_x = 0; + + + + queue_draw(); + + return Gtk::TreeView::on_motion_notify_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e) +{ + SPFilterPrimitive *prim = get_selected(), *target; + + _scroll_connection.disconnect(); + + if(_in_drag && prim) { + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + int cx, cy; + + if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) { + const gchar *in_val = nullptr; + Glib::ustring result; + Gtk::TreeIter target_iter = _model->get_iter(path); + target = (*target_iter)[_columns.primitive]; + col = get_column(1); + + Gdk::Rectangle rct; + get_cell_area(path, *col, rct); + const int twidth = get_input_type_width(); + const int sources_x = rct.get_width() - twidth * FPInputConverter._length; + if(cx > sources_x) { + int src = (cx - sources_x) / twidth; + if (src < 0) { + src = 0; + } else if(src >= static_cast(FPInputConverter._length)) { + src = FPInputConverter._length - 1; + } + result = FPInputConverter.get_key((FilterPrimitiveInput)src); + in_val = result.c_str(); + } + else { + // Ensure that the target comes before the selected primitive + for(Gtk::TreeIter iter = _model->children().begin(); + iter != get_selection()->get_selected(); ++iter) { + if(iter == target_iter) { + Inkscape::XML::Node *repr = target->getRepr(); + // Make sure the target has a result + const gchar *gres = repr->attribute("result"); + if(!gres) { + result = SP_FILTER(prim->parent)->get_new_result_name(); + repr->setAttributeOrRemoveIfEmpty("result", result); + in_val = result.c_str(); + } + else + in_val = gres; + break; + } + } + } + + if(SP_IS_FEMERGE(prim)) { + int c = 1; + bool handled = false; + for (auto& o: prim->children) { + if(c == _in_drag && SP_IS_FEMERGENODE(&o)) { + // If input is null, delete it + if(!in_val) { + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(o.getRepr()); + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove merge node")); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } else { + _dialog.set_attr(&o, SP_ATTR_IN, in_val); + } + handled = true; + break; + } + ++c; + } + // Add new input? + if(!handled && c == _in_drag && in_val) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode"); + repr->setAttribute("inkscape:collect", "always"); + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + SPFeMergeNode *node = SP_FEMERGENODE(prim->document->getObjectByRepr(repr)); + Inkscape::GC::release(repr); + _dialog.set_attr(node, SP_ATTR_IN, in_val); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } + } + else { + if(_in_drag == 1) + _dialog.set_attr(prim, SP_ATTR_IN, in_val); + else if(_in_drag == 2) + _dialog.set_attr(prim, SP_ATTR_IN2, in_val); + } + } + + _in_drag = 0; + queue_draw(); + + _dialog.update_settings_view(); + } + + if((e->type == GDK_BUTTON_RELEASE) && (e->button == 3)) { + const bool sensitive = get_selected() != nullptr; + auto items = _primitive_menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + + _primitive_menu->popup_at_pointer(reinterpret_cast(e)); + + return true; + } + else + return Gtk::TreeView::on_button_release_event(e); +} + +// Checks all of prim's inputs, removes any that use result +static void check_single_connection(SPFilterPrimitive* prim, const int result) +{ + if (prim && (result >= 0)) { + if (prim->image_in == result) { + prim->removeAttribute("in"); + } + + if (SP_IS_FEBLEND(prim)) { + if (SP_FEBLEND(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FECOMPOSITE(prim)) { + if (SP_FECOMPOSITE(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FEDISPLACEMENTMAP(prim)) { + if (SP_FEDISPLACEMENTMAP(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } + } +} + +// Remove any connections going to/from prim_iter that forward-reference other primitives +void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter) +{ + SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive]; + bool before = true; + + for(Gtk::TreeIter iter = _model->children().begin(); + iter != _model->children().end(); ++iter) { + if(iter == prim_iter) + before = false; + else { + SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive]; + if(before) + check_single_connection(cur_prim, prim->image_out); + else + check_single_connection(prim, cur_prim->image_out); + } + } +} + +// Reorder the filter primitives to match the list order +void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr& /*dc*/) +{ + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + int ndx = 0; + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + prim->getRepr()->setPosition(ndx); + break; + } + } + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + sanitize_connections(iter); + get_selection()->select(iter); + break; + } + } + + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Reorder filter primitive")); +} + +// If a connection is dragged towards the top or bottom of the list, the list should scroll to follow. +bool FilterEffectsDialog::PrimitiveList::on_scroll_timeout() +{ + if(_autoscroll_y) { + auto a = dynamic_cast(get_parent())->get_vadjustment(); + double v = a->get_value() + _autoscroll_y; + + if(v < 0) + v = 0; + if(v > a->get_upper() - a->get_page_size()) + v = a->get_upper() - a->get_page_size(); + + a->set_value(v); + + queue_draw(); + } + + + if(_autoscroll_x) { + auto a_h = dynamic_cast(get_parent())->get_hadjustment(); + double h = a_h->get_value() + _autoscroll_x; + + if(h < 0) + h = 0; + if(h > a_h->get_upper() - a_h->get_page_size()) + h = a_h->get_upper() - a_h->get_page_size(); + + a_h->set_value(h); + + queue_draw(); + } + + return true; +} + +int FilterEffectsDialog::PrimitiveList::primitive_count() const +{ + return _model->children().size(); +} + +int FilterEffectsDialog::PrimitiveList::get_input_type_width() const +{ + // Maximum font height calculated in initText() and stored in _input_type_width. + // Add 2 to font height to account for rectangle around text. + return _input_type_width + 2; +} + +/*** FilterEffectsDialog ***/ + +FilterEffectsDialog::FilterEffectsDialog() + : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS), + _add_primitive_type(FPConverter), + _add_primitive(_("Add Effect:")), + _empty_settings(_("No effect selected"), Gtk::ALIGN_START), + _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START), + _settings_initialized(false), + _locked(false), + _attr_lock(false), + _filter_modifier(*this), + _primitive_list(*this) +{ + _settings = new Settings(*this, _settings_tab1, sigc::mem_fun(*this, &FilterEffectsDialog::set_attr_direct), + NR_FILTER_ENDPRIMITIVETYPE); + _filter_general_settings = new Settings(*this, _settings_tab2, sigc::mem_fun(*this, &FilterEffectsDialog::set_filternode_attr), + 1); + + // Initialize widget hierarchy + auto hpaned = Gtk::manage(new Gtk::Paned()); + _primitive_box = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_VERTICAL)); + + _sw_infobox = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::ScrolledWindow* sw_prims = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::HBox* infobox = Gtk::manage(new Gtk::HBox(/*homogeneous:*/false, /*spacing:*/4)); + Gtk::HBox* hb_prims = Gtk::manage(new Gtk::HBox); + Gtk::VBox* vb_prims = Gtk::manage(new Gtk::VBox); + Gtk::VBox* vb_desc = Gtk::manage(new Gtk::VBox); + + Gtk::VBox* prim_vbox_p = Gtk::manage(new Gtk::VBox); + Gtk::VBox* prim_vbox_i = Gtk::manage(new Gtk::VBox); + + sw_prims->add(_primitive_list); + + prim_vbox_p->pack_start(*sw_prims, true, true); + prim_vbox_i->pack_start(*vb_prims, true, true); + + _primitive_box->pack1(*prim_vbox_p); + _primitive_box->pack2(*prim_vbox_i, false, false); + + hpaned->pack1(_filter_modifier); + hpaned->pack2(*_primitive_box); + _getContents()->add(*hpaned); + + _infobox_icon.set_halign(Gtk::ALIGN_START); + _infobox_icon.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_halign(Gtk::ALIGN_START); + _infobox_desc.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_justify(Gtk::JUSTIFY_LEFT); + _infobox_desc.set_line_wrap(true); + _infobox_desc.set_size_request(300, -1); + + vb_desc->pack_start(_infobox_desc, true, true); + + infobox->pack_start(_infobox_icon, false, false); + infobox->pack_start(*vb_desc, true, true); + + //_sw_infobox->set_size_request(-1, -1); + _sw_infobox->set_size_request(300, -1); + _sw_infobox->add(*infobox); + + //vb_prims->set_size_request(-1, 50); + vb_prims->pack_start(*hb_prims, false, false); + vb_prims->pack_start(*_sw_infobox, true, true); + + hb_prims->pack_start(_add_primitive, false, false); + hb_prims->pack_start(_add_primitive_type, true, true); + _getContents()->pack_start(_settings_tabs, false, false); + _settings_tabs.append_page(_settings_tab1, _("Effect parameters")); + _settings_tabs.append_page(_settings_tab2, _("Filter General Settings")); + + _primitive_list.signal_primitive_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_settings_view)); + _filter_modifier.signal_filter_changed().connect( + sigc::mem_fun(_primitive_list, &PrimitiveList::update)); + + _add_primitive_type.signal_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_primitive_infobox)); + + sw_prims->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + sw_prims->set_shadow_type(Gtk::SHADOW_IN); + _sw_infobox->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + +// al_settings->set_padding(0, 0, 12, 0); +// fr_settings->set_shadow_type(Gtk::SHADOW_NONE); +// ((Gtk::Label*)fr_settings->get_label_widget())->set_use_markup(); + _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive)); + _primitive_list.set_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive), + sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected)); + + show_all_children(); + init_settings_widgets(); + _primitive_list.update(); + update_primitive_infobox(); +} + +FilterEffectsDialog::~FilterEffectsDialog() +{ + delete _settings; + delete _filter_general_settings; +} + +void FilterEffectsDialog::set_attrs_locked(const bool l) +{ + _locked = l; +} + +void FilterEffectsDialog::show_all_vfunc() +{ + UI::Widget::Panel::show_all_vfunc(); + + update_settings_view(); +} + +void FilterEffectsDialog::init_settings_widgets() +{ + // TODO: Find better range/climb-rate/digits values for the SpinScales, + // most of the current values are complete guesses! + + _settings_tab1.set_border_width(4); + _settings_tab2.set_border_width(4); + + _empty_settings.set_sensitive(false); + _settings_tab1.pack_start(_empty_settings); + + _no_filter_selected.set_sensitive(false); + _settings_tab2.pack_start(_no_filter_selected); + _settings_initialized = true; + + _filter_general_settings->type(0); + _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SP_ATTR_X, SP_ATTR_Y, _("Coordinates:"), -100, 100, 0.01, 0.1, 2, _("X coordinate of the left corners of filter effects region"), _("Y coordinate of the upper corners of filter effects region")); + _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SP_ATTR_WIDTH, SP_ATTR_HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region")); + + _settings->type(NR_FILTER_BLEND); + _settings->add_combo(SP_CSS_BLEND_NORMAL, SP_ATTR_MODE, _("Mode:"), SPBlendModeConverter); + + _settings->type(NR_FILTER_COLORMATRIX); + ComboBoxEnum* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SP_ATTR_TYPE, _("Type:"), ColorMatrixTypeConverter, _("Indicates the type of matrix operation. The keyword 'matrix' indicates that a full 5x4 matrix of values will be provided. The other keywords represent convenience shortcuts to allow commonly used color operations to be performed without specifying a complete matrix.")); + _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s):")); + colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix)); + + _settings->type(NR_FILTER_COMPONENTTRANSFER); + _settings->add_componenttransfervalues(_("R:"), SPFeFuncNode::R); + _settings->add_componenttransfervalues(_("G:"), SPFeFuncNode::G); + _settings->add_componenttransfervalues(_("B:"), SPFeFuncNode::B); + _settings->add_componenttransfervalues(_("A:"), SPFeFuncNode::A); + + _settings->type(NR_FILTER_COMPOSITE); + _settings->add_combo(COMPOSITE_OVER, SP_ATTR_OPERATOR, _("Operator:"), CompositeOperatorConverter); + _k1 = _settings->add_spinscale(0, SP_ATTR_K1, _("K1:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k2 = _settings->add_spinscale(0, SP_ATTR_K2, _("K2:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k3 = _settings->add_spinscale(0, SP_ATTR_K3, _("K3:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k4 = _settings->add_spinscale(0, SP_ATTR_K4, _("K4:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + + _settings->type(NR_FILTER_CONVOLVEMATRIX); + _convolve_order = _settings->add_dualspinbutton((char*)"3", SP_ATTR_ORDER, _("Size:"), 1, 5, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix")); + _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SP_ATTR_TARGETX, SP_ATTR_TARGETY, _("Target:"), 0, 4, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point.")); + //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix) + _convolve_matrix = _settings->add_matrix(SP_ATTR_KERNELMATRIX, _("Kernel:"), _("This matrix describes the convolve operation that is applied to the input image in order to calculate the pixel colors at the output. Different arrangements of values in this matrix result in various possible visual effects. An identity matrix would lead to a motion blur effect (parallel to the matrix diagonal) while a matrix filled with a constant non-zero value would lead to a common blur effect.")); + _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed)); + _settings->add_spinscale(0, SP_ATTR_DIVISOR, _("Divisor:"), 0, 1000, 1, 0.1, 2, _("After applying the kernelMatrix to the input image to yield a number, that number is divided by divisor to yield the final destination color value. A divisor that is the sum of all the matrix values tends to have an evening effect on the overall color intensity of the result.")); + _settings->add_spinscale(0, SP_ATTR_BIAS, _("Bias:"), -10, 10, 1, 0.01, 1, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter.")); + _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_DUPLICATE, SP_ATTR_EDGEMODE, _("Edge Mode:"), ConvolveMatrixEdgeModeConverter, _("Determines how to extend the input image as necessary with color values so that the matrix operations can be applied when the kernel is positioned at or near the edge of the input image.")); + _settings->add_checkbutton(false, SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + + _settings->type(NR_FILTER_DIFFUSELIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SP_PROP_LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_SURFACESCALE, _("Surface Scale:"), -5, 5, 0.01, 0.001, 3, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SP_ATTR_DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_dualspinscale(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_DISPLACEMENTMAP); + _settings->add_spinscale(0, SP_ATTR_SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect.")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_YCHANNELSELECTOR, _("Y displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the Y direction")); + + _settings->type(NR_FILTER_FLOOD); + _settings->add_color(/*default: black*/ 0, SP_PROP_FLOOD_COLOR, _("Flood Color:"), _("The whole filter region will be filled with this color.")); + _settings->add_spinscale(1, SP_PROP_FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2); + + _settings->type(NR_FILTER_GAUSSIANBLUR); + _settings->add_dualspinscale(SP_ATTR_STDDEVIATION, _("Standard Deviation:"), 0.01, 100, 1, 0.01, 2, _("The standard deviation for the blur operation.")); + + _settings->type(NR_FILTER_MERGE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_MORPHOLOGY); + _settings->add_combo(MORPHOLOGY_OPERATOR_ERODE, SP_ATTR_OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattenning\" of input image.")); + _settings->add_dualspinscale(SP_ATTR_RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1); + + _settings->type(NR_FILTER_IMAGE); + _settings->add_fileorelement(SP_ATTR_XLINK_HREF, _("Source of Image:")); + _image_x = _settings->add_entry(SP_ATTR_X,_("X"),_("X")); + _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed)); + //This commented because we want the default empty value of X or Y and couldent get it from SpinButton + //_image_y = _settings->add_spinbutton(0, SP_ATTR_Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y")); + _image_y = _settings->add_entry(SP_ATTR_Y,_("Y"),_("Y")); + _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed)); + _settings->type(NR_FILTER_OFFSET); + _settings->add_checkbutton(false, SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + _settings->add_spinscale(0, SP_ATTR_DX, _("Delta X:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted to the right")); + _settings->add_spinscale(0, SP_ATTR_DY, _("Delta Y:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted downwards")); + + _settings->type(NR_FILTER_SPECULARLIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SP_PROP_LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_SURFACESCALE, _("Surface Scale:"), -5, 5, 0.1, 0.01, 2, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SP_ATTR_SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_spinscale(1, SP_ATTR_SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\".")); + _settings->add_dualspinscale(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_TILE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_TURBULENCE); +// _settings->add_checkbutton(false, SP_ATTR_STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch"); + _settings->add_combo(TURBULENCE_TURBULENCE, SP_ATTR_TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function.")); + _settings->add_dualspinscale(SP_ATTR_BASEFREQUENCY, _("Base Frequency:"), 0, 1, 0.001, 0.01, 3); + _settings->add_spinscale(1, SP_ATTR_NUMOCTAVES, _("Octaves:"), 1, 10, 1, 1, 0); + _settings->add_spinscale(0, SP_ATTR_SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator.")); +} + +void FilterEffectsDialog::add_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + SPFilterPrimitive* prim = filter_add_primitive(filter, _add_primitive_type.get_active_data()->id); + + _primitive_list.select(prim); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter primitive")); + } +} + +void FilterEffectsDialog::update_primitive_infobox() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + switch(_add_primitive_type.get_active_data()->id){ + case(NR_FILTER_BLEND): + _infobox_icon.set_from_icon_name("feBlend-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feBlend filter primitive provides 4 image blending modes: screen, multiply, darken and lighten.")); + break; + case(NR_FILTER_COLORMATRIX): + _infobox_icon.set_from_icon_name("feColorMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feColorMatrix filter primitive applies a matrix transformation to color of each rendered pixel. This allows for effects like turning object to grayscale, modifying color saturation and changing color hue.")); + break; + case(NR_FILTER_COMPONENTTRANSFER): + _infobox_icon.set_from_icon_name("feComponentTransfer-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feComponentTransfer filter primitive manipulates the input's color components (red, green, blue, and alpha) according to particular transfer functions, allowing operations like brightness and contrast adjustment, color balance, and thresholding.")); + break; + case(NR_FILTER_COMPOSITE): + _infobox_icon.set_from_icon_name("feComposite-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feComposite filter primitive composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard. Porter-Duff blending modes are essentially logical operations between the corresponding pixel values of the images.")); + break; + case(NR_FILTER_CONVOLVEMATRIX): + _infobox_icon.set_from_icon_name("feConvolveMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feConvolveMatrix lets you specify a Convolution to be applied on the image. Common effects created using convolution matrices are blur, sharpening, embossing and edge detection. Note that while gaussian blur can be created using this filter primitive, the special gaussian blur primitive is faster and resolution-independent.")); + break; + case(NR_FILTER_DIFFUSELIGHTING): + _infobox_icon.set_from_icon_name("feDiffuseLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feDiffuseLighting and feSpecularLighting filter primitives create \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_DISPLACEMENTMAP): + _infobox_icon.set_from_icon_name("feDisplacementMap-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feDisplacementMap filter primitive displaces the pixels in the first input using the second input as a displacement map, that shows from how far the pixel should come from. Classical examples are whirl and pinch effects.")); + break; + case(NR_FILTER_FLOOD): + _infobox_icon.set_from_icon_name("feFlood-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feFlood filter primitive fills the region with a given color and opacity. It is usually used as an input to other filters to apply color to a graphic.")); + break; + case(NR_FILTER_GAUSSIANBLUR): + _infobox_icon.set_from_icon_name("feGaussianBlur-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feGaussianBlur filter primitive uniformly blurs its input. It is commonly used together with feOffset to create a drop shadow effect.")); + break; + case(NR_FILTER_IMAGE): + _infobox_icon.set_from_icon_name("feImage-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feImage filter primitive fills the region with an external image or another part of the document.")); + break; + case(NR_FILTER_MERGE): + _infobox_icon.set_from_icon_name("feMerge-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feMerge filter primitive composites several temporary images inside the filter primitive to a single image. It uses normal alpha compositing for this. This is equivalent to using several feBlend primitives in 'normal' mode or several feComposite primitives in 'over' mode.")); + break; + case(NR_FILTER_MORPHOLOGY): + _infobox_icon.set_from_icon_name("feMorphology-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feMorphology filter primitive provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.")); + break; + case(NR_FILTER_OFFSET): + _infobox_icon.set_from_icon_name("feOffset-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feOffset filter primitive offsets the image by an user-defined amount. For example, this is useful for drop shadows, where the shadow is in a slightly different position than the actual object.")); + break; + case(NR_FILTER_SPECULARLIGHTING): + _infobox_icon.set_from_icon_name("feSpecularLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feDiffuseLighting and feSpecularLighting filter primitives create \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_TILE): + _infobox_icon.set_from_icon_name("feTile-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feTile filter primitive tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.")); + break; + case(NR_FILTER_TURBULENCE): + _infobox_icon.set_from_icon_name("feTurbulence-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The feTurbulence filter primitive renders Perlin noise. This kind of noise is useful in simulating several nature phenomena like clouds, fire and smoke and in generating complex textures like marble or granite.")); + break; + default: + g_assert(false); + break; + } + //_infobox_icon.set_pixel_size(96); + _infobox_icon.set_pixel_size(64); +} + +void FilterEffectsDialog::duplicate_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + SPFilterPrimitive* origprim = _primitive_list.get_selected(); + + if (filter && origprim) { + Inkscape::XML::Node *repr; + repr = origprim->getRepr()->duplicate(origprim->getRepr()->document()); + filter->getRepr()->appendChild(repr); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter primitive")); + + _primitive_list.update(); + } +} + +void FilterEffectsDialog::convolve_order_changed() +{ + _convolve_matrix->set_from_attribute(_primitive_list.get_selected()); + _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1); + _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1); +} + +bool number_or_empy(const Glib::ustring& text) { + if (text.empty()) { + return true; + } + double n = atof( text.c_str() ); + if (n == 0.0 && strcmp(text.c_str(), "0") != 0 && strcmp(text.c_str(), "0.0") != 0) { + return false; + } + else { + return true; + } +} + +void FilterEffectsDialog::image_x_changed() +{ + if (number_or_empy(_image_x->get_text())) { + _image_x->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::image_y_changed() +{ + if (number_or_empy(_image_y->get_text())) { + _image_y->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::set_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_filternode_attr(const AttrWidget* input) +{ + if(!_locked) { + _attr_lock = true; + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(input->get_attribute()); + if (filter && name && filter->getRepr()){ + filter->setAttributeOrRemoveIfEmpty(name, input->get_as_attribute()); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + _attr_lock = false; + } +} + +void FilterEffectsDialog::set_child_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected()->firstChild(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_attr(SPObject* o, const SPAttributeEnum attr, const gchar* val) +{ + if(!_locked) { + _attr_lock = true; + + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(attr); + if(filter && name && o) { + update_settings_sensitivity(); + + o->setAttribute(name, val); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "filtereffects:"; + undokey += name; + DocumentUndo::maybeDone(filter->document, undokey.c_str(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Set filter primitive attribute")); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_filter_general_settings_view() +{ + if(_settings_initialized != true) return; + + if(!_locked) { + _attr_lock = true; + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + else { + std::vector vect = _settings_tab2.get_children(); + vect[0]->hide(); + _no_filter_selected.show(); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_settings_view() +{ + update_settings_sensitivity(); + + if(_attr_lock) + return; + +//First Tab + + std::vector vect1 = _settings_tab1.get_children(); + for(auto & i : vect1) + i->hide(); + _empty_settings.show(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + + SPFilterPrimitive* prim = _primitive_list.get_selected(); + + if(prim && prim->getRepr()) { + + //XML Tree being used directly here while it shouldn't be. + _settings->show_and_update(FPConverter.get_id_from_key(prim->getRepr()->name()), prim); + _empty_settings.hide(); + } + +//Second Tab + + std::vector vect2 = _settings_tab2.get_children(); + vect2[0]->hide(); + _no_filter_selected.show(); + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + +} + +void FilterEffectsDialog::update_settings_sensitivity() +{ + SPFilterPrimitive* prim = _primitive_list.get_selected(); + const bool use_k = SP_IS_FECOMPOSITE(prim) && SP_FECOMPOSITE(prim)->composite_operator == COMPOSITE_ARITHMETIC; + _k1->set_sensitive(use_k); + _k2->set_sensitive(use_k); + _k3->set_sensitive(use_k); + _k4->set_sensitive(use_k); + +} + +void FilterEffectsDialog::update_color_matrix() +{ + _color_matrix_values->set_from_attribute(_primitive_list.get_selected()); +} + +} // 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 : diff --git a/src/ui/dialog/filter-effects-dialog.h b/src/ui/dialog/filter-effects-dialog.h new file mode 100644 index 0000000..0ed28d1 --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.h @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Effects dialog + */ +/* Authors: + * Nicholas Bishop + * Rodrigo Kumpera + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H +#define INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H + +#include + +#include + +#include +#include + +#include "attributes.h" +#include "display/nr-filter-types.h" +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/combo-enums.h" +#include "ui/widget/panel.h" +#include "ui/widget/spin-scale.h" + +#include "xml/helper-observer.h" + +class SPFilter; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class EntryAttr; +//class SpinButtonAttr; +class DualSpinButton; +class MultiSpinButton; +class FilterEffectsDialog : public UI::Widget::Panel { +public: + + FilterEffectsDialog(); + ~FilterEffectsDialog() override; + + static FilterEffectsDialog &getInstance() + { return *new FilterEffectsDialog(); } + + void set_attrs_locked(const bool); +protected: + void show_all_vfunc() override; +private: + + class FilterModifier : public Gtk::VBox + { + public: + FilterModifier(FilterEffectsDialog&); + ~FilterModifier() override; + + SPFilter* get_selected_filter(); + void select_filter(const SPFilter*); + + sigc::signal& signal_filter_changed() + { + return _signal_filter_changed; + } + private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(filter); + add(label); + add(sel); + add(count); + } + + Gtk::TreeModelColumn filter; + Gtk::TreeModelColumn label; + Gtk::TreeModelColumn sel; + Gtk::TreeModelColumn count; + }; + + void setTargetDesktop(SPDesktop *desktop); + + void on_document_replaced(SPDesktop *desktop, SPDocument *document); + void on_change_selection(); + void on_modified_selection( guint flags ); + + void update_selection(Selection *); + void on_filter_selection_changed(); + + void on_name_edited(const Glib::ustring&, const Glib::ustring&); + bool on_filter_move(const Glib::RefPtr& /*context*/, int x, int y, guint /*time*/); + void on_selection_toggled(const Glib::ustring&); + + void update_counts(); + void update_filters(); + void filter_list_button_release(GdkEventButton*); + void add_filter(); + void remove_filter(); + void duplicate_filter(); + void rename_filter(); + + /** + * Stores the current desktop. + */ + SPDesktop *_desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker _deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection _selectChangedConn; + sigc::connection _selectModifiedConn; + sigc::connection _doc_replaced; + sigc::connection _resource_changed; + + FilterEffectsDialog& _dialog; + Gtk::TreeView _list; + Glib::RefPtr _model; + Columns _columns; + Gtk::CellRendererToggle _cell_toggle; + Gtk::Button _add; + Gtk::Menu *_menu; + sigc::signal _signal_filter_changed; + std::unique_ptr _observer; + }; + + class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PrimitiveColumns() + { + add(primitive); + add(type_id); + add(type); + add(id); + } + + Gtk::TreeModelColumn primitive; + Gtk::TreeModelColumn type_id; + Gtk::TreeModelColumn type; + Gtk::TreeModelColumn id; + }; + + class CellRendererConnection : public Gtk::CellRenderer + { + public: + CellRendererConnection(); + Glib::PropertyProxy property_primitive(); + + static const int size = 24; + + protected: + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int height, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const override; + + void get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int width, + int& minimum_height, + int& natural_height) const override; + private: + // void* should be SPFilterPrimitive*, some weirdness with properties prevents this + Glib::Property _primitive; + int _text_width; + }; + + class PrimitiveList : public Gtk::TreeView + { + public: + PrimitiveList(FilterEffectsDialog&); + + sigc::signal& signal_primitive_changed(); + + void update(); + void set_menu(Gtk::Widget &parent, + sigc::slot dup, + sigc::slot rem); + + SPFilterPrimitive* get_selected(); + void select(SPFilterPrimitive *prim); + void remove_selected(); + + int primitive_count() const; + int get_input_type_width() const; + + protected: + bool on_draw_signal(const Cairo::RefPtr &cr); + + + bool on_button_press_event(GdkEventButton*) override; + bool on_motion_notify_event(GdkEventMotion*) override; + bool on_button_release_event(GdkEventButton*) override; + void on_drag_end(const Glib::RefPtr&) override; + private: + void init_text(); + + void draw_connection_node(const Cairo::RefPtr& cr, + const std::vector& points, + const bool fill); + + bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector& points, + const int ix, const int iy); + + const Gtk::TreeIter find_result(const Gtk::TreeIter& start, const int attr, int& src_id); + int find_index(const Gtk::TreeIter& target); + void draw_connection(const Cairo::RefPtr& cr, + const Gtk::TreeIter&, const int attr, const int text_start_x, + const int x1, const int y1, const int row_count, + const GdkRGBA fg_color, const GdkRGBA mid_color); + void sanitize_connections(const Gtk::TreeIter& prim_iter); + void on_primitive_selection_changed(); + bool on_scroll_timeout(); + + FilterEffectsDialog& _dialog; + Glib::RefPtr _model; + PrimitiveColumns _columns; + CellRendererConnection _connection_cell; + Gtk::Menu *_primitive_menu; + Glib::RefPtr _vertical_layout; + int _in_drag; + SPFilterPrimitive* _drag_prim; + sigc::signal _signal_primitive_changed; + sigc::connection _scroll_connection; + int _autoscroll_y; + int _autoscroll_x; + std::unique_ptr _observer; + int _input_type_width; + int _input_type_height; + }; + + void init_settings_widgets(); + + // Handlers + void add_primitive(); + void remove_primitive(); + void duplicate_primitive(); + void convolve_order_changed(); + void image_x_changed(); + void image_y_changed(); + + void set_attr_direct(const UI::Widget::AttrWidget*); + void set_child_attr_direct(const UI::Widget::AttrWidget*); + void set_filternode_attr(const UI::Widget::AttrWidget*); + void set_attr(SPObject*, const SPAttributeEnum, const gchar* val); + void update_settings_view(); + void update_filter_general_settings_view(); + void update_settings_sensitivity(); + void update_color_matrix(); + void update_primitive_infobox(); + + // Primitives Info Box + Gtk::Label _infobox_desc; + Gtk::Image _infobox_icon; + Gtk::ScrolledWindow* _sw_infobox; + + // View/add primitives + Gtk::Paned* _primitive_box; + + UI::Widget::ComboBoxEnum _add_primitive_type; + Gtk::Button _add_primitive; + + // Bottom pane (filter effect primitive settings) + Gtk::Notebook _settings_tabs; + Gtk::VBox _settings_tab2; + Gtk::VBox _settings_tab1; + Gtk::Label _empty_settings; + Gtk::Label _no_filter_selected; + bool _settings_initialized; + + class Settings; + class MatrixAttr; + class ColorMatrixValues; + class ComponentTransferValues; + class LightSourceControl; + Settings* _settings; + Settings* _filter_general_settings; + + // Color Matrix + ColorMatrixValues* _color_matrix_values; + + // Component Transfer + ComponentTransferValues* _component_transfer_values; + + // Convolve Matrix + MatrixAttr* _convolve_matrix; + DualSpinButton* _convolve_order; + MultiSpinButton* _convolve_target; + + // Image + EntryAttr* _image_x; + EntryAttr* _image_y; + + // For controlling setting sensitivity + Gtk::Widget* _k1, *_k2, *_k3, *_k4; + + // To prevent unwanted signals + bool _locked; + bool _attr_lock; + + // These go last since they depend on the prior initialization of + // other FilterEffectsDialog members + FilterModifier _filter_modifier; + PrimitiveList _primitive_list; + + FilterEffectsDialog(FilterEffectsDialog const &d); + FilterEffectsDialog& operator=(FilterEffectsDialog const &d); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H + +/* + 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 : diff --git a/src/ui/dialog/find.cpp b/src/ui/dialog/find.cpp new file mode 100644 index 0000000..a6d53bb --- /dev/null +++ b/src/ui/dialog/find.cpp @@ -0,0 +1,1076 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce W. Harrington + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "find.h" + +#include +#include +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "text-editing.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-line.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-polyline.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "object/sp-text.h" +#include "object/sp-tref.h" +#include "object/sp-tspan.h" +#include "object/sp-use.h" + +#include "ui/dialog-events.h" + +#include "xml/attribute-record.h" +#include "xml/node-iterators.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Find::Find() + : UI::Widget::Panel("/dialogs/find", SP_VERB_DIALOG_FIND), + + entry_find(_("F_ind:"), _("Find objects by their content or properties (exact or partial match)")), + entry_replace(_("R_eplace:"), _("Replace match with this value")), + + check_scope_all(_("_All")), + check_scope_layer(_("Current _layer")), + check_scope_selection(_("Sele_ction")), + check_searchin_text(_("_Text")), + check_searchin_property(_("_Properties")), + vbox_searchin(false, false), + frame_searchin(_("Search in")), + frame_scope(_("Scope")), + + check_case_sensitive(_("Case sensiti_ve")), + check_exact_match(_("E_xact match")), + check_include_hidden(_("Include _hidden")), + check_include_locked(_("Include loc_ked")), + expander_options(_("Options")), + frame_options(_("General")), + + check_ids(_("_ID")), + check_attributename(_("Attribute _name")), + check_attributevalue(_("Attri_bute value")), + check_style(_("_Style")), + check_font(_("F_ont")), + frame_properties(_("Properties")), + + check_alltypes(_("All types")), + check_rects(_("Rectangles")), + check_ellipses(_("Ellipses")), + check_stars(_("Stars")), + check_spirals(_("Spirals")), + check_paths(_("Paths")), + check_texts(_("Texts")), + check_groups(_("Groups")), + check_clones( + //TRANSLATORS: "Clones" is a noun indicating type of object to find + C_("Find dialog", "Clones")), + + check_images(_("Images")), + check_offsets(_("Offsets")), + frame_types(_("Object types")), + + status(""), + button_find(_("_Find")), + button_replace(_("_Replace All")), + _action_replace(false), + blocked(false), + desktop(nullptr), + deskTrack() + +{ + _left_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + _right_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + button_find.set_use_underline(); + button_find.set_tooltip_text(_("Select all objects matching the selection criteria")); + button_replace.set_use_underline(); + button_replace.set_tooltip_text(_("Replace all matches")); + check_scope_all.set_use_underline(); + check_scope_all.set_tooltip_text(_("Search in all layers")); + check_scope_layer.set_use_underline(); + check_scope_layer.set_tooltip_text(_("Limit search to the current layer")); + check_scope_selection.set_use_underline(); + check_scope_selection.set_tooltip_text(_("Limit search to the current selection")); + check_searchin_text.set_use_underline(); + check_searchin_text.set_tooltip_text(_("Search in text objects")); + check_searchin_property.set_use_underline(); + check_searchin_property.set_tooltip_text(_("Search in object properties, styles, attributes and IDs")); + check_case_sensitive.set_use_underline(); + check_case_sensitive.set_tooltip_text(_("Match upper/lower case")); + check_case_sensitive.set_active(false); + check_exact_match.set_use_underline(); + check_exact_match.set_tooltip_text(_("Match whole objects only")); + check_exact_match.set_active(false); + check_include_hidden.set_use_underline(); + check_include_hidden.set_tooltip_text(_("Include hidden objects in search")); + check_include_hidden.set_active(false); + check_include_locked.set_use_underline(); + check_include_locked.set_tooltip_text(_("Include locked objects in search")); + check_include_locked.set_active(false); + check_ids.set_use_underline(); + check_ids.set_tooltip_text(_("Search id name")); + check_ids.set_active(true); + check_attributename.set_use_underline(); + check_attributename.set_tooltip_text(_("Search attribute name")); + check_attributename.set_active(false); + check_attributevalue.set_use_underline(); + check_attributevalue.set_tooltip_text(_("Search attribute value")); + check_attributevalue.set_active(true); + check_style.set_use_underline(); + check_style.set_tooltip_text(_("Search style")); + check_style.set_active(true); + check_font.set_use_underline(); + check_font.set_tooltip_text(_("Search fonts")); + check_font.set_active(false); + check_alltypes.set_use_underline(); + check_alltypes.set_tooltip_text(_("Search all object types")); + check_alltypes.set_active(true); + check_rects.set_use_underline(); + check_rects.set_tooltip_text(_("Search rectangles")); + check_rects.set_active(false); + check_ellipses.set_use_underline(); + check_ellipses.set_tooltip_text(_("Search ellipses, arcs, circles")); + check_ellipses.set_active(false); + check_stars.set_use_underline(); + check_stars.set_tooltip_text(_("Search stars and polygons")); + check_stars.set_active(false); + check_spirals.set_use_underline(); + check_spirals.set_tooltip_text(_("Search spirals")); + check_spirals.set_active(false); + check_paths.set_use_underline(); + check_paths.set_tooltip_text(_("Search paths, lines, polylines")); + check_paths.set_active(false); + check_texts.set_use_underline(); + check_texts.set_tooltip_text(_("Search text objects")); + check_texts.set_active(false); + check_groups.set_use_underline(); + check_groups.set_tooltip_text(_("Search groups")); + check_groups.set_active(false), + check_clones.set_use_underline(); + check_clones.set_tooltip_text(_("Search clones")); + check_clones.set_active(false); + check_images.set_use_underline(); + check_images.set_tooltip_text(_("Search images")); + check_images.set_active(false); + check_offsets.set_use_underline(); + check_offsets.set_tooltip_text(_("Search offset objects")); + check_offsets.set_active(false); + + entry_find.getEntry()->set_width_chars(25); + entry_find.child_property_fill(*entry_find.getEntry()) = true; + entry_find.child_property_expand(*entry_find.getEntry()) = true; + entry_replace.getEntry()->set_width_chars(25); + entry_replace.child_property_fill(*entry_replace.getEntry()) = true; + entry_replace.child_property_expand(*entry_replace.getEntry()) = true; + + Gtk::RadioButtonGroup grp_searchin = check_searchin_text.get_group(); + check_searchin_property.set_group(grp_searchin); + vbox_searchin.pack_start(check_searchin_text, Gtk::PACK_SHRINK); + vbox_searchin.pack_start(check_searchin_property, Gtk::PACK_SHRINK); + frame_searchin.add(vbox_searchin); + + Gtk::RadioButtonGroup grp_scope = check_scope_all.get_group(); + check_scope_layer.set_group(grp_scope); + check_scope_selection.set_group(grp_scope); + vbox_scope.pack_start(check_scope_all, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_layer, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_selection, Gtk::PACK_SHRINK); + hbox_searchin.set_spacing(12); + hbox_searchin.pack_start(frame_searchin, Gtk::PACK_SHRINK); + hbox_searchin.pack_start(frame_scope, Gtk::PACK_SHRINK); + frame_scope.add(vbox_scope); + + vbox_options1.pack_start(check_case_sensitive, Gtk::PACK_SHRINK); + vbox_options1.pack_start(check_include_hidden, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_exact_match, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_include_locked, Gtk::PACK_SHRINK); + _left_size_group->add_widget(check_case_sensitive); + _left_size_group->add_widget(check_include_hidden); + _right_size_group->add_widget(check_exact_match); + _right_size_group->add_widget(check_include_locked); + hbox_options.set_spacing(4); + hbox_options.pack_start(vbox_options1, Gtk::PACK_SHRINK); + hbox_options.pack_start(vbox_options2, Gtk::PACK_SHRINK); + frame_options.add(hbox_options); + + vbox_properties1.pack_start(check_ids, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_style, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_font, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributevalue, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributename, Gtk::PACK_SHRINK); + vbox_properties2.set_valign(Gtk::ALIGN_START); + _left_size_group->add_widget(check_ids); + _left_size_group->add_widget(check_style); + _left_size_group->add_widget(check_font); + _right_size_group->add_widget(check_attributevalue); + _right_size_group->add_widget(check_attributename); + hbox_properties.set_spacing(4); + hbox_properties.pack_start(vbox_properties1, Gtk::PACK_SHRINK); + hbox_properties.pack_start(vbox_properties2, Gtk::PACK_SHRINK); + frame_properties.add(hbox_properties); + + vbox_types1.pack_start(check_alltypes, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_paths, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_texts, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_groups, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_clones, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_images, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_offsets, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_rects, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_ellipses, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_stars, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_spirals, Gtk::PACK_SHRINK); + vbox_types2.set_valign(Gtk::ALIGN_END); + _left_size_group->add_widget(check_alltypes); + _left_size_group->add_widget(check_paths); + _left_size_group->add_widget(check_texts); + _left_size_group->add_widget(check_groups); + _left_size_group->add_widget(check_clones); + _left_size_group->add_widget(check_images); + _right_size_group->add_widget(check_offsets); + _right_size_group->add_widget(check_rects); + _right_size_group->add_widget(check_ellipses); + _right_size_group->add_widget(check_stars); + _right_size_group->add_widget(check_spirals); + hbox_types.set_spacing(4); + hbox_types.pack_start(vbox_types1, Gtk::PACK_SHRINK); + hbox_types.pack_start(vbox_types2, Gtk::PACK_SHRINK); + frame_types.add(hbox_types); + + vbox_expander.set_spacing(4); + vbox_expander.pack_start(frame_options, true, true); + vbox_expander.pack_start(frame_properties, true, true); + vbox_expander.pack_start(frame_types, true, true); + + expander_options.set_use_underline(); + expander_options.add(vbox_expander); + + box_buttons.set_layout(Gtk::BUTTONBOX_END); + box_buttons.set_spacing(6); + box_buttons.pack_start(button_find, true, true); + box_buttons.pack_start(button_replace, true, true); + hboxbutton_row.set_spacing(6); + hboxbutton_row.pack_start(status, true, true); + hboxbutton_row.pack_end(box_buttons, true, true); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(6); + contents->pack_start(entry_find, false, false); + contents->pack_start(entry_replace, false, false); + contents->pack_start(hbox_searchin, false, false); + contents->pack_start(expander_options, false, false); + contents->pack_end(hboxbutton_row, false, false); + + checkProperties.push_back(&check_ids); + checkProperties.push_back(&check_style); + checkProperties.push_back(&check_font); + checkProperties.push_back(&check_attributevalue); + checkProperties.push_back(&check_attributename); + + checkTypes.push_back(&check_paths); + checkTypes.push_back(&check_texts); + checkTypes.push_back(&check_groups); + checkTypes.push_back(&check_clones); + checkTypes.push_back(&check_images); + checkTypes.push_back(&check_offsets); + checkTypes.push_back(&check_rects); + checkTypes.push_back(&check_ellipses); + checkTypes.push_back(&check_stars); + checkTypes.push_back(&check_spirals); + + // set signals to handle clicks + expander_options.property_expanded().signal_changed().connect(sigc::mem_fun(*this, &Find::onExpander)); + button_find.signal_clicked().connect(sigc::mem_fun(*this, &Find::onFind)); + button_replace.signal_clicked().connect(sigc::mem_fun(*this, &Find::onReplace)); + check_searchin_text.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinText)); + check_searchin_property.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinProperty)); + check_alltypes.signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleAlltypes)); + + for (auto & checkProperty : checkProperties) { + checkProperty->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + for (auto & checkType : checkTypes) { + checkType->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + onSearchinText(); + onToggleAlltypes(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Find::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + SPItem *item = selection->singleItem(); + if (item) { + if (dynamic_cast(item) || dynamic_cast(item)) { + gchar *str; + str = sp_te_get_string_multiline (item); + entry_find.getEntry()->set_text(str); + } + } + + button_find.set_can_default(); + //button_find.grab_default(); // activatable by Enter + entry_find.getEntry()->grab_focus(); +} + +Find::~Find() +{ + desktopChangeConn.disconnect(); + selectChangedConn.disconnect(); + deskTrack.disconnect(); +} + +void Find::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Find::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Find::onSelectionChange))); + } + } +} + +void Find::onSelectionChange() +{ + if (!blocked) { + status.set_text(""); + } +} + +/*######################################################################## +# FIND helper functions +########################################################################*/ + +Glib::ustring Find::find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall) +{ + Glib::ustring ustr = str; + Glib::ustring ufind = find; + if (!casematch) { + ufind = ufind.lowercase(); + } + gsize n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch); + while (n != std::string::npos) { + ustr.replace(n, ufind.length(), replace); + if (!replaceall) { + return ustr; + } + // Start the next search after the last replace character to avoid infinite loops (replace "a" with "aaa" etc) + n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch, n + strlen(replace) + 1); + } + return ustr; +} + +gsize Find::find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start/*=0*/) +{ + Glib::ustring ustr = str ? str : ""; + Glib::ustring ufind = find; + + if (!casematch) { + ustr = ustr.lowercase(); + ufind = ufind.lowercase(); + } + + gsize pos = std::string::npos; + if (exact) { + if (ustr == ufind) { + pos = 0; + } + } else { + pos = ustr.find(ufind, start); + } + + return pos; +} + + +bool Find::find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch) +{ + return (std::string::npos != find_strcmp_pos(str, find, exact, casematch)); +} + +bool Find::item_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast(item) || dynamic_cast(item)) { + const gchar *item_text = sp_te_get_string_multiline (item); + if (item_text == nullptr) { + return false; + } + bool found = find_strcmp(item_text, find, exact, casematch); + + if (found && replace) { + Glib::ustring ufind = find; + if (!casematch) { + ufind = ufind.lowercase(); + } + + Inkscape::Text::Layout const *layout = te_get_layout (item); + if (!layout) { + return found; + } + + gchar* replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gsize n = find_strcmp_pos(item_text, ufind.c_str(), exact, casematch); + static Inkscape::Text::Layout::iterator _begin_w; + static Inkscape::Text::Layout::iterator _end_w; + while (n != std::string::npos) { + _begin_w = layout->charIndexToIterator(n); + _end_w = layout->charIndexToIterator(n + strlen(find)); + sp_te_replace(item, _begin_w, _end_w, replace_text); + item_text = sp_te_get_string_multiline (item); + n = find_strcmp_pos(item_text, ufind.c_str(), exact, casematch, n + strlen(replace_text) + 1); + } + + g_free(replace_text); + } + + return found; + } + return false; +} + + +bool Find::item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast(item)) { // SPStrings have "on demand" ids which are useless for searching + return false; + } + + const gchar *item_id = item->getRepr()->attribute("id"); + if (item_id == nullptr) { + return false; + } + + bool found = find_strcmp(item_id, id, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_id, id, replace_text , exact, casematch, true); + if (new_item_style != item_id) { + item->setAttribute("id", new_item_style); + } + g_free(replace_text); + } + + return found; +} + +bool Find::item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + gchar *item_style = g_strdup(item->getRepr()->attribute("style")); + if (item_style == nullptr) { + return false; + } + + bool found = find_strcmp(item_style, text, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_style, text, replace_text , exact, casematch, true); + if (new_item_style != item_style) { + item->setAttribute("style", new_item_style); + } + g_free(replace_text); + } + + g_free(item_style); + return found; +} + +bool Find::item_attr_match(SPItem *item, const gchar *text, bool exact, bool /*casematch*/, bool replace/*=false*/) +{ + bool found = false; + + if (item->getRepr() == nullptr) { + return false; + } + + gchar *attr_value = g_strdup(item->getRepr()->attribute(text)); + if (exact) { + found = (attr_value != nullptr); + } else { + found = item->getRepr()->matchAttributeName(text); + } + g_free(attr_value); + + // TODO - Rename attribute name ? + if (found && replace) { + found = false; + } + + return found; +} + +bool Find::item_attrvalue_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + Inkscape::Util::List iter = item->getRepr()->attributeList(); + for (; iter; ++iter) { + const gchar* key = g_quark_to_string(iter->key); + gchar *attr_value = g_strdup(item->getRepr()->attribute(key)); + bool found = find_strcmp(attr_value, text, exact, casematch); + if (found) { + ret = true; + } + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(attr_value, text, replace_text , exact, casematch, true); + if (new_item_style != attr_value) { + item->setAttribute(key, new_item_style); + } + } + + g_free(attr_value); + } + + return ret; +} + + +bool Find::item_font_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool /*replace*/ /*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + const gchar *item_style = item->getRepr()->attribute("style"); + if (item_style == nullptr) { + return false; + } + + std::vector vFontTokenNames; + vFontTokenNames.emplace_back("font-family:"); + vFontTokenNames.emplace_back("-inkscape-font-specification:"); + + std::vector vStyleTokens = Glib::Regex::split_simple(";", item_style); + for (auto & vStyleToken : vStyleTokens) { + Glib::ustring token = vStyleToken; + for (const auto & vFontTokenName : vFontTokenNames) { + if ( token.find(vFontTokenName) != std::string::npos) { + Glib::ustring font1 = Glib::ustring(vFontTokenName).append(text); + bool found = find_strcmp(token.c_str(), font1.c_str(), exact, casematch); + if (found) { + ret = true; + if (_action_replace) { + gchar *replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gchar *orig_str = g_strdup(token.c_str()); + // Exact match fails since the "font-family:" is in the token, since the find was exact it still works with false below + Glib::ustring new_item_style = find_replace(orig_str, text, replace_text , false /*exact*/, casematch, true); + if (new_item_style != orig_str) { + vStyleToken = new_item_style; + } + g_free(orig_str); + g_free(replace_text); + } + } + } + } + } + + if (ret && _action_replace) { + Glib::ustring new_item_style; + for (const auto & vStyleToken : vStyleTokens) { + new_item_style.append(vStyleToken).append(";"); + } + new_item_style.erase(new_item_style.size()-1); + item->setAttribute("style", new_item_style); + } + + return ret; +} + + +std::vector Find::filter_fields (std::vector &l, bool exact, bool casematch) +{ + Glib::ustring tmp = entry_find.getEntry()->get_text(); + if (tmp.empty()) { + return l; + } + gchar* text = g_strdup(tmp.c_str()); + + std::vector in = l; + std::vector out; + + if (check_searchin_text.get_active()) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_text_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_text_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + else if (check_searchin_property.get_active()) { + + bool ids = check_ids.get_active(); + bool style = check_style.get_active(); + bool font = check_font.get_active(); + bool attrname = check_attributename.get_active(); + bool attrvalue = check_attributevalue.get_active(); + + if (ids) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + if (item_id_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_id_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (style) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_style_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)){ + out.push_back(*i); + if (_action_replace) { + item_style_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrname) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_attr_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attr_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrvalue) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_attrvalue_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attrvalue_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (font) { + for (std::vector::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_font_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(),*i)) { + out.push_back(*i); + if (_action_replace) { + item_font_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + } + + g_free(text); + + return out; +} + + +bool Find::item_type_match (SPItem *item) +{ + bool all =check_alltypes.get_active(); + + if ( dynamic_cast(item)) { + return ( all ||check_rects.get_active()); + + } else if (dynamic_cast(item)) { + return ( all || check_ellipses.get_active()); + + } else if (dynamic_cast(item) || dynamic_cast(item)) { + return ( all || check_stars.get_active()); + + } else if (dynamic_cast(item)) { + return ( all || check_spirals.get_active()); + + } else if (dynamic_cast(item) || dynamic_cast(item) || dynamic_cast(item)) { + return (all || check_paths.get_active()); + + } else if (dynamic_cast(item) || dynamic_cast(item) || + dynamic_cast(item) || dynamic_cast(item) || + dynamic_cast(item) || dynamic_cast(item) || + dynamic_cast(item) || dynamic_cast(item)) { + return (all || check_texts.get_active()); + + } else if (dynamic_cast(item) && !desktop->isLayer(item) ) { // never select layers! + return (all || check_groups.get_active()); + + } else if (dynamic_cast(item)) { + return (all || check_clones.get_active()); + + } else if (dynamic_cast(item)) { + return (all || check_images.get_active()); + + } else if (dynamic_cast(item)) { + return (all || check_offsets.get_active()); + } + + return false; +} + +std::vector Find::filter_types (std::vector &l) +{ + std::vector n; + for (std::vector::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item_type_match(item)) { + n.push_back(*i); + } + } + return n; +} + + +std::vector &Find::filter_list (std::vector &l, bool exact, bool casematch) +{ + l = filter_types (l); + l = filter_fields (l, exact, casematch); + return l; +} + +std::vector &Find::all_items (SPObject *r, std::vector &l, bool hidden, bool locked) +{ + if (dynamic_cast(r)) { + return l; // we're not interested in items in defs + } + + if (!strcmp(r->getRepr()->name(), "svg:metadata")) { + return l; // we're not interested in metadata + } + + for (auto& child: r->children) { + SPItem *item = dynamic_cast(&child); + if (item && !child.cloned && !desktop->isLayer(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.insert(l.begin(),(SPItem*)&child); + } + } + l = all_items (&child, l, hidden, locked); + } + return l; +} + +std::vector &Find::all_selection_items (Inkscape::Selection *s, std::vector &l, SPObject *ancestor, bool hidden, bool locked) +{ + auto itemlist= s->items(); + for (auto i=boost::rbegin(itemlist); boost::rend(itemlist) != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + if (item && !item->cloned && !desktop->isLayer(item)) { + if (!ancestor || ancestor->isAncestorOf(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.push_back(*i); + } + } + } + if (!ancestor || ancestor->isAncestorOf(item)) { + l = all_items(item, l, hidden, locked); + } + } + return l; +} + + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void Find::onFind() +{ + _action_replace = false; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onReplace() +{ + if (entry_find.getEntry()->get_text().length() < 1) { + status.set_text(_("Nothing to replace")); + return; + } + _action_replace = true; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onAction() +{ + + bool hidden = check_include_hidden.get_active(); + bool locked = check_include_locked.get_active(); + bool exact = check_exact_match.get_active(); + bool casematch = check_case_sensitive.get_active(); + blocked = true; + + std::vector l; + if (check_scope_selection.get_active()) { + if (check_scope_layer.get_active()) { + l = all_selection_items (desktop->selection, l, desktop->currentLayer(), hidden, locked); + } else { + l = all_selection_items (desktop->selection, l, nullptr, hidden, locked); + } + } else { + if (check_scope_layer.get_active()) { + l = all_items (desktop->currentLayer(), l, hidden, locked); + } else { + l = all_items(desktop->getDocument()->getRoot(), l, hidden, locked); + } + } + guint all = l.size(); + + std::vector n = filter_list (l, exact, casematch); + + if (!n.empty()) { + int count = n.size(); + desktop->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, + // TRANSLATORS: "%s" is replaced with "exact" or "partial" when this string is displayed + ngettext("%d object found (out of %d), %s match.", + "%d objects found (out of %d), %s match.", + count), + count, all, exact? _("exact") : _("partial")); + if (_action_replace){ + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 match replaced","%1 matches replaced",count), count)); + } + else { + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 object found","%1 objects found",count), count)); + bool attributenameyok = !check_attributename.get_active(); + button_replace.set_sensitive(attributenameyok); + } + + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(n); + SPObject *obj = n[0]; + SPItem *item = dynamic_cast(obj); + g_assert(item != nullptr); + scroll_to_show_item(desktop, item); + + if (_action_replace) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Replace text or property")); + } + + } else { + status.set_text(_("Nothing found")); + if (!check_scope_selection.get_active()) { + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + } + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No objects found")); + } + blocked = false; + +} + +void Find::onToggleCheck () +{ + bool objectok = false; + status.set_text(""); + + if (check_alltypes.get_active()) { + objectok = true; + } + for (auto & checkType : checkTypes) { + if (checkType->get_active()) { + objectok = true; + } + } + + if (!objectok) { + status.set_text(_("Select an object type")); + } + + + bool propertyok = false; + + if (!check_searchin_property.get_active()) { + propertyok = true; + } else { + + for (auto & checkProperty : checkProperties) { + if (checkProperty->get_active()) { + propertyok = true; + } + } + } + + if (!propertyok) { + status.set_text(_("Select a property")); + } + + // Can't replace attribute names + // bool attributenameyok = !check_attributename.get_active(); + + button_find.set_sensitive(objectok && propertyok); + // button_replace.set_sensitive(objectok && propertyok && attributenameyok); + button_replace.set_sensitive(false); +} + +void Find::onToggleAlltypes () +{ + bool all =check_alltypes.get_active(); + for (auto & checkType : checkTypes) { + checkType->set_sensitive(!all); + } + + onToggleCheck(); +} + +void Find::onSearchinText () +{ + searchinToggle(false); + onToggleCheck(); +} + +void Find::onSearchinProperty () +{ + searchinToggle(true); + onToggleCheck(); +} + +void Find::searchinToggle(bool on) +{ + for (auto & checkProperty : checkProperties) { + checkProperty->set_sensitive(on); + } +} + +void Find::onExpander () +{ + if (!expander_options.get_expanded()) + squeeze_window(); +} + +/*######################################################################## +# UTILITY +########################################################################*/ + +void Find::squeeze_window() +{ + // TODO: resize dialog window when the expander is closed + // set_size_request(-1, -1); +} + +} // 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 : diff --git a/src/ui/dialog/find.h b/src/ui/dialog/find.h new file mode 100644 index 0000000..0eeeee0 --- /dev/null +++ b/src/ui/dialog/find.h @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Find dialog + */ +/* Authors: + * Bryce W. Harrington + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FIND_H +#define INKSCAPE_UI_DIALOG_FIND_H + +#include "ui/widget/panel.h" +#include "ui/widget/entry.h" +#include "ui/widget/frame.h" + +#include +#include +#include +#include +#include +#include + +#include "ui/dialog/desktop-tracker.h" + +class SPItem; +class SPObject; + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Dialog { + +/** + * The Find class defines the Find and replace dialog. + * + * The Find and replace dialog allows you to search within the + * current document for specific text or properties of items. + * Matches items are highlighted and can be replaced as well. + * Scope can be limited to the entire document, current layer or selected items. + * Other options allow searching on specific object types and properties. + */ + +class Find : public UI::Widget::Panel { +public: + Find(); + ~Find() override; + + /** + * Helper function which returns a new instance of the dialog. + * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager). + */ + static Find &getInstance() { return *new Find(); } + +protected: + + + /** + * Callbacks for pressing the dialog buttons. + */ + void onFind(); + void onReplace(); + void onExpander(); + void onAction(); + void onToggleAlltypes(); + void onToggleCheck(); + void onSearchinText(); + void onSearchinProperty(); + + /** + * Toggle all the properties checkboxes + */ + void searchinToggle(bool on); + + /** + * Returns true if the SPItem 'item' has the same id + * + * @param item the SPItem to check + * @param id the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text content + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + * + */ + bool item_text_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text in the style attribute + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if found the SPItem 'item' has the same attribute name + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attr_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same attribute value + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attrvalue_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same font values + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_font_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Function to filter a list of items based on the item type by calling each item_XXX_match function + */ + std::vector filter_fields (std::vector &l, bool exact, bool casematch); + bool item_type_match (SPItem *item); + std::vector filter_types (std::vector &l); + std::vector & filter_list (std::vector &l, bool exact, bool casematch); + + /** + * Find a string within a string and returns true if found with options for exact and casematching + */ + bool find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch); + + /** + * Find a string within a string and return the position with options for exact, casematching and search start location + */ + gsize find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start=0); + + /** + * Replace a string with another string with options for exact and casematching and replace once/all + */ + Glib::ustring find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall); + + /** + * recursive function to return a list of all the items in the SPObject tree + * + */ + std::vector & all_items (SPObject *r, std::vector &l, bool hidden, bool locked); + /** + * to return a list of all the selected items + * + */ + std::vector & all_selection_items (Inkscape::Selection *s, std::vector &l, SPObject *ancestor, bool hidden, bool locked); + + /** + * Shrink the dialog size when the expander widget is closed + * Currently not working, no known way to do this + */ + void squeeze_window(); + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Called when desktop selection changes + */ + void onSelectionChange(); + +private: + Find(Find const &d) = delete; + Find& operator=(Find const &d) = delete; + + /* + * Find and replace combo box widgets + */ + UI::Widget::Entry entry_find; + UI::Widget::Entry entry_replace; + + /** + * Scope and search in widgets + */ + Gtk::RadioButton check_scope_all; + Gtk::RadioButton check_scope_layer; + Gtk::RadioButton check_scope_selection; + Gtk::RadioButton check_searchin_text; + Gtk::RadioButton check_searchin_property; + Gtk::HBox hbox_searchin; + Gtk::VBox vbox_scope; + Gtk::VBox vbox_searchin; + UI::Widget::Frame frame_searchin; + UI::Widget::Frame frame_scope; + + /** + * General option widgets + */ + Gtk::CheckButton check_case_sensitive; + Gtk::CheckButton check_exact_match; + Gtk::CheckButton check_include_hidden; + Gtk::CheckButton check_include_locked; + Gtk::VBox vbox_options1; + Gtk::VBox vbox_options2; + Gtk::HBox hbox_options; + Gtk::VBox vbox_expander; + Gtk::Expander expander_options; + UI::Widget::Frame frame_options; + + /** + * Property type widgets + */ + Gtk::CheckButton check_ids; + Gtk::CheckButton check_attributename; + Gtk::CheckButton check_attributevalue; + Gtk::CheckButton check_style; + Gtk::CheckButton check_font; + Gtk::HBox hbox_properties; + Gtk::VBox vbox_properties1; + Gtk::VBox vbox_properties2; + UI::Widget::Frame frame_properties; + + /** + * A vector of all the properties widgets for easy processing + */ + std::vector checkProperties; + + /** + * Object type widgets + */ + Gtk::CheckButton check_alltypes; + Gtk::CheckButton check_rects; + Gtk::CheckButton check_ellipses; + Gtk::CheckButton check_stars; + Gtk::CheckButton check_spirals; + Gtk::CheckButton check_paths; + Gtk::CheckButton check_texts; + Gtk::CheckButton check_groups; + Gtk::CheckButton check_clones; + Gtk::CheckButton check_images; + Gtk::CheckButton check_offsets; + Gtk::VBox vbox_types1; + Gtk::VBox vbox_types2; + Gtk::HBox hbox_types; + UI::Widget::Frame frame_types; + + Glib::RefPtr _left_size_group; + Glib::RefPtr _right_size_group; + + /** + * A vector of all the check option widgets for easy processing + */ + std::vector checkTypes; + + //Gtk::HBox hbox_text; + + /** + * Action Buttons and status + */ + Gtk::Label status; + Gtk::Button button_find; + Gtk::Button button_replace; + Gtk::ButtonBox box_buttons; + Gtk::HBox hboxbutton_row; + + /** + * Finding or replacing + */ + bool _action_replace; + bool blocked; + + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FIND_H + +/* + 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 : diff --git a/src/ui/dialog/floating-behavior.cpp b/src/ui/dialog/floating-behavior.cpp new file mode 100644 index 0000000..804f1f0 --- /dev/null +++ b/src/ui/dialog/floating-behavior.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Floating dialog implementation. + */ +/* Author: + * Gustav Broberg + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "floating-behavior.h" +#include "dialog.h" + +#include "inkscape.h" +#include "desktop.h" +#include "ui/dialog-events.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +FloatingBehavior::FloatingBehavior(Dialog &dialog) : + Behavior(dialog), + _d (new Gtk::Dialog(_dialog._title)) + ,_dialog_active(_d->property_is_active()) + ,_trans_focus(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-focus", 0.95, 0.0, 1.0)) + ,_trans_blur(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-blur", 0.50, 0.0, 1.0)) + ,_trans_time(Inkscape::Preferences::get()->getIntLimited("/dialogs/transparency/animate-time", 100, 0, 5000)) +{ + hide(); + + signal_delete_event().connect(sigc::mem_fun(_dialog, &Inkscape::UI::Dialog::Dialog::_onDeleteEvent)); + + sp_transientize(GTK_WIDGET(_d->gobj())); + _dialog.retransientize_suppress = false; +} + +FloatingBehavior::~FloatingBehavior() +{ + delete _d; + _d = nullptr; +} + +Behavior * +FloatingBehavior::create(Dialog &dialog) +{ + return new FloatingBehavior(dialog); +} + +inline FloatingBehavior::operator Gtk::Widget &() { return *_d; } +inline GtkWidget *FloatingBehavior::gobj() { return GTK_WIDGET(_d->gobj()); } +inline Gtk::Box* FloatingBehavior::get_vbox() { + return _d->get_content_area(); +} +inline void FloatingBehavior::present() { _d->present(); } +inline void FloatingBehavior::hide() { _d->hide(); } +inline void FloatingBehavior::show() { _d->show(); } +inline void FloatingBehavior::show_all_children() { _d->show_all_children(); } +inline void FloatingBehavior::resize(int width, int height) { _d->resize(width, height); } +inline void FloatingBehavior::move(int x, int y) { _d->move(x, y); } +inline void FloatingBehavior::set_position(Gtk::WindowPosition position) { _d->set_position(position); } +inline void FloatingBehavior::set_size_request(int width, int height) { _d->set_size_request(width, height); } +inline void FloatingBehavior::size_request(Gtk::Requisition &requisition) { + Gtk::Requisition requisition_natural; + _d->get_preferred_size(requisition, requisition_natural); +} +inline void FloatingBehavior::get_position(int &x, int &y) { _d->get_position(x, y); } +inline void FloatingBehavior::get_size(int &width, int &height) { _d->get_size(width, height); } +inline void FloatingBehavior::set_title(Glib::ustring title) { _d->set_title(title); } +inline void FloatingBehavior::set_sensitive(bool sensitive) { _d->set_sensitive(sensitive); } + +Glib::SignalProxy0 FloatingBehavior::signal_show() { return _d->signal_show(); } +Glib::SignalProxy0 FloatingBehavior::signal_hide() { return _d->signal_hide(); } +Glib::SignalProxy1 FloatingBehavior::signal_delete_event () { return _d->signal_delete_event(); } + + +void +FloatingBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +FloatingBehavior::onShowF12() +{ + show(); + _dialog.read_geometry(); +} + +void +FloatingBehavior::onShutdown() +{ + _dialog.save_status(!_dialog._user_hidden, 1, GDL_DOCK_TOP); // Make sure 1 == DockItem::FLOATING_STATE +} + +void +FloatingBehavior::onDesktopActivated (SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + GtkWindow *dialog_win = GTK_WINDOW(_d->gobj()); + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (dialog_win) + { + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); +} + + +} // namespace Behavior +} // 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 : diff --git a/src/ui/dialog/floating-behavior.h b/src/ui/dialog/floating-behavior.h new file mode 100644 index 0000000..d4e9ebe --- /dev/null +++ b/src/ui/dialog/floating-behavior.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A floating dialog implementation. + */ +/* Author: + * Gustav Broberg + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H + +#include +#include "behavior.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class FloatingBehavior : public Behavior { + +public: + static Behavior *create(Dialog &dialog); + + ~FloatingBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget &() override; + GtkWidget *gobj() override; + void present() override; + Gtk::Box *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition &requisition) override; + void get_position(int &x, int &y) override; + void get_size(int& width, int &height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0 signal_show() override; + Glib::SignalProxy0 signal_hide() override; + Glib::SignalProxy1 signal_delete_event() override; + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + FloatingBehavior(Dialog& dialog); + + Gtk::Dialog *_d; //< the actual dialog + + Glib::PropertyProxy_ReadOnly _dialog_active; //< Variable proxy to track whether the dialog is the active window + float _trans_focus; //< The percentage opacity when the dialog is focused + float _trans_blur; //< The percentage opactiy when the dialog is not focused + int _trans_time; //< The amount of time (in ms) for the dialog to change it's transparency +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H + +/* + 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 : diff --git a/src/ui/dialog/font-substitution.cpp b/src/ui/dialog/font-substitution.cpp new file mode 100644 index 0000000..bf91259 --- /dev/null +++ b/src/ui/dialog/font-substitution.cpp @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include + +#include +#include +#include +#include + +#include "font-substitution.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "text-editing.h" + +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" +#include "object/sp-tspan.h" + +#include "libnrtype/FontFactory.h" +#include "libnrtype/font-instance.h" + +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FontSubstitution::FontSubstitution() += default; + +FontSubstitution::~FontSubstitution() += default; + +void +FontSubstitution::checkFontSubstitutions(SPDocument* doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_dlg = prefs->getInt("/options/font/substitutedlg", 0); + if (show_dlg) { + Glib::ustring out; + std::vector l = getFontReplacedItems(doc, &out); + if (out.length() > 0) { + show(out, l); + } + } +} + +void +FontSubstitution::show(Glib::ustring out, std::vector &l) +{ + Gtk::MessageDialog warning(_("\nSome fonts are not available and have been substituted."), + false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true); + warning.set_resizable(true); + warning.set_title(_("Font substitution")); + + GtkWidget *dlg = GTK_WIDGET(warning.gobj()); + sp_transientize(dlg); + + Gtk::TextView * textview = new Gtk::TextView(); + textview->set_editable(false); + textview->set_wrap_mode(Gtk::WRAP_WORD); + textview->show(); + textview->get_buffer()->set_text(_(out.c_str())); + + Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow(); + scrollwindow->add(*textview); + scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollwindow->set_shadow_type(Gtk::SHADOW_IN); + scrollwindow->set_size_request(0, 100); + scrollwindow->show(); + + Gtk::CheckButton *cbSelect = new Gtk::CheckButton(); + cbSelect->set_label(_("Select all the affected items")); + cbSelect->set_active(true); + cbSelect->show(); + + Gtk::CheckButton *cbWarning = new Gtk::CheckButton(); + cbWarning->set_label(_("Don't show this warning again")); + cbWarning->show(); + + auto box = warning.get_content_area(); + box->set_spacing(2); + box->pack_start(*scrollwindow, true, true, 4); + box->pack_start(*cbSelect, false, false, 0); + box->pack_start(*cbWarning, false, false, 0); + + warning.run(); + + if (cbWarning->get_active()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/options/font/substitutedlg", 0); + } + + if (cbSelect->get_active()) { + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(l); + } + +} + +/* + * Find all the fonts that are in the document but not available on the users system + * and have been substituted for other fonts + * + * Return a list of SPItems where fonts have been substituted. + * + * Walk through all the objects ... + * a. Build up a list of the objects with fonts defined in the style attribute + * b. Build up a list of the objects rendered fonts - taken for the objects layout/spans + * If there are fonts in a. that are not in b. then those fonts have been substituted. + */ +std::vector FontSubstitution::getFontReplacedItems(SPDocument* doc, Glib::ustring *out) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + std::vector allList; + std::vector outList,x,y; + std::set setErrors; + std::set setFontSpans; + std::map mapFontStyles; + + allList = get_all_items(x, doc->getRoot(), desktop, false, false, true, y); + for(auto item : allList){ + SPStyle *style = item->style; + Glib::ustring family = ""; + + if (is_top_level_text_object (item)) { + // Should only need to check the first span, since the others should be covered by TSPAN's etc + family = te_get_layout(item)->getFontFamily(0); + setFontSpans.insert(family); + } + else if (SP_IS_TEXTPATH(item)) { + SPTextPath const *textpath = SP_TEXTPATH(item); + if (textpath->originalPath != nullptr) { + family = SP_TEXT(item->parent)->layout.getFontFamily(0); + setFontSpans.insert(family); + } + } + else if (SP_IS_TSPAN(item) || SP_IS_FLOWTSPAN(item)) { + // is_part_of_text_subtree (item) + // TSPAN layout comes from the parent->layout->_spans + SPObject *parent_text = item; + while (parent_text && !SP_IS_TEXT(parent_text)) { + parent_text = parent_text->parent; + } + if (parent_text != nullptr) { + family = SP_TEXT(parent_text)->layout.getFontFamily(0); + // Add all the spans fonts to the set + for (unsigned int f=0; f < parent_text->children.size(); f++) { + family = SP_TEXT(parent_text)->layout.getFontFamily(f); + setFontSpans.insert(family); + } + } + } + + if (style) { + gchar const *style_font = nullptr; + if (style->font_family.set) + style_font = style->font_family.value(); + else if (style->font_specification.set) + style_font = style->font_specification.value(); + else + style_font = style->font_family.value(); + + if (style_font) { + if (has_visible_text(item)) { + mapFontStyles.insert(std::make_pair (item, style_font)); + } + } + } + } + + // Check if any document styles are not in the actual layout + std::map::const_reverse_iterator mapIter; + for (mapIter = mapFontStyles.rbegin(); mapIter != mapFontStyles.rend(); ++mapIter) { + SPItem *item = mapIter->first; + Glib::ustring fonts = mapIter->second; + + // CSS font fallbacks can have more that one font listed, split the font list + std::vector vFonts = Glib::Regex::split_simple("," , fonts); + bool fontFound = false; + for(auto font : vFonts) { + // trim whitespace + size_t startpos = font.find_first_not_of(" \n\r\t"); + size_t endpos = font.find_last_not_of(" \n\r\t"); + if(( std::string::npos == startpos ) || ( std::string::npos == endpos)) { + continue; // empty font name + } + font = font.substr( startpos, endpos-startpos+1 ); + std::set::const_iterator iter = setFontSpans.find(font); + if (iter != setFontSpans.end() || + font == Glib::ustring("sans-serif") || + font == Glib::ustring("Sans") || + font == Glib::ustring("serif") || + font == Glib::ustring("Serif") || + font == Glib::ustring("monospace") || + font == Glib::ustring("Monospace")) { + fontFound = true; + break; + } + } + if (fontFound == false) { + Glib::ustring subName = getSubstituteFontName(fonts); + Glib::ustring err = Glib::ustring::compose( + _("Font '%1' substituted with '%2'"), fonts.c_str(), subName.c_str()); + setErrors.insert(err); + outList.push_back(item); + } + } + + std::set::const_iterator setIter; + for (setIter = setErrors.begin(); setIter != setErrors.end(); ++setIter) { + Glib::ustring err = (*setIter); + out->append(err + "\n"); + g_warning("%s", err.c_str()); + } + + return outList; +} + + +Glib::ustring FontSubstitution::getSubstituteFontName (Glib::ustring font) +{ + Glib::ustring out = font; + + PangoFontDescription *descr = pango_font_description_new(); + pango_font_description_set_family(descr,font.c_str()); + font_instance *res = (font_factory::Default())->Face(descr); + if (res->pFont) { + PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont); + out = sp_font_description_get_family(nFaceDesc); + } + pango_font_description_free(descr); + + return out; +} + + +} // 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 : diff --git a/src/ui/dialog/font-substitution.h b/src/ui/dialog/font-substitution.h new file mode 100644 index 0000000..107c534 --- /dev/null +++ b/src/ui/dialog/font-substitution.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief FontSubstitution dialog + */ +/* Authors: + * + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_FONT_SUBSTITUTION_H +#define INKSCAPE_UI_FONT_SUBSTITUTION_H + +#include + +class SPItem; +class SPDocument; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FontSubstitution { +public: + FontSubstitution(); + virtual ~FontSubstitution(); + void checkFontSubstitutions(SPDocument* doc); + void show(Glib::ustring out, std::vector &l); + + static FontSubstitution &getInstance() { return *new FontSubstitution(); } + Glib::ustring getSubstituteFontName (Glib::ustring font); + +protected: + std::vector getFontReplacedItems(SPDocument* doc, Glib::ustring *out); + +private: + FontSubstitution(FontSubstitution const &d) = delete; + FontSubstitution& operator=(FontSubstitution const &d) = delete; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_FONT_SUBSTITUTION_H + +/* + 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 : diff --git a/src/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp new file mode 100644 index 0000000..0603f48 --- /dev/null +++ b/src/ui/dialog/glyphs.cpp @@ -0,0 +1,822 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "glyphs.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" // for SPDocumentUndo::done() +#include "selection.h" +#include "text-editing.h" +#include "verbs.h" + +#include "libnrtype/font-instance.h" +#include "libnrtype/font-lister.h" + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" + +#include "ui/widget/font-selector.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +GlyphsPanel &GlyphsPanel::getInstance() +{ + return *new GlyphsPanel(); +} + + +static std::map & getScriptToName() +{ + static bool init = false; + static std::map mappings; + if (!init) { + init = true; + mappings[G_UNICODE_SCRIPT_INVALID_CODE] = _("all"); + mappings[G_UNICODE_SCRIPT_COMMON] = _("common"); + mappings[G_UNICODE_SCRIPT_INHERITED] = _("inherited"); + mappings[G_UNICODE_SCRIPT_ARABIC] = _("Arabic"); + mappings[G_UNICODE_SCRIPT_ARMENIAN] = _("Armenian"); + mappings[G_UNICODE_SCRIPT_BENGALI] = _("Bengali"); + mappings[G_UNICODE_SCRIPT_BOPOMOFO] = _("Bopomofo"); + mappings[G_UNICODE_SCRIPT_CHEROKEE] = _("Cherokee"); + mappings[G_UNICODE_SCRIPT_COPTIC] = _("Coptic"); + mappings[G_UNICODE_SCRIPT_CYRILLIC] = _("Cyrillic"); + mappings[G_UNICODE_SCRIPT_DESERET] = _("Deseret"); + mappings[G_UNICODE_SCRIPT_DEVANAGARI] = _("Devanagari"); + mappings[G_UNICODE_SCRIPT_ETHIOPIC] = _("Ethiopic"); + mappings[G_UNICODE_SCRIPT_GEORGIAN] = _("Georgian"); + mappings[G_UNICODE_SCRIPT_GOTHIC] = _("Gothic"); + mappings[G_UNICODE_SCRIPT_GREEK] = _("Greek"); + mappings[G_UNICODE_SCRIPT_GUJARATI] = _("Gujarati"); + mappings[G_UNICODE_SCRIPT_GURMUKHI] = _("Gurmukhi"); + mappings[G_UNICODE_SCRIPT_HAN] = _("Han"); + mappings[G_UNICODE_SCRIPT_HANGUL] = _("Hangul"); + mappings[G_UNICODE_SCRIPT_HEBREW] = _("Hebrew"); + mappings[G_UNICODE_SCRIPT_HIRAGANA] = _("Hiragana"); + mappings[G_UNICODE_SCRIPT_KANNADA] = _("Kannada"); + mappings[G_UNICODE_SCRIPT_KATAKANA] = _("Katakana"); + mappings[G_UNICODE_SCRIPT_KHMER] = _("Khmer"); + mappings[G_UNICODE_SCRIPT_LAO] = _("Lao"); + mappings[G_UNICODE_SCRIPT_LATIN] = _("Latin"); + mappings[G_UNICODE_SCRIPT_MALAYALAM] = _("Malayalam"); + mappings[G_UNICODE_SCRIPT_MONGOLIAN] = _("Mongolian"); + mappings[G_UNICODE_SCRIPT_MYANMAR] = _("Myanmar"); + mappings[G_UNICODE_SCRIPT_OGHAM] = _("Ogham"); + mappings[G_UNICODE_SCRIPT_OLD_ITALIC] = _("Old Italic"); + mappings[G_UNICODE_SCRIPT_ORIYA] = _("Oriya"); + mappings[G_UNICODE_SCRIPT_RUNIC] = _("Runic"); + mappings[G_UNICODE_SCRIPT_SINHALA] = _("Sinhala"); + mappings[G_UNICODE_SCRIPT_SYRIAC] = _("Syriac"); + mappings[G_UNICODE_SCRIPT_TAMIL] = _("Tamil"); + mappings[G_UNICODE_SCRIPT_TELUGU] = _("Telugu"); + mappings[G_UNICODE_SCRIPT_THAANA] = _("Thaana"); + mappings[G_UNICODE_SCRIPT_THAI] = _("Thai"); + mappings[G_UNICODE_SCRIPT_TIBETAN] = _("Tibetan"); + mappings[G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL] = _("Canadian Aboriginal"); + mappings[G_UNICODE_SCRIPT_YI] = _("Yi"); + mappings[G_UNICODE_SCRIPT_TAGALOG] = _("Tagalog"); + mappings[G_UNICODE_SCRIPT_HANUNOO] = _("Hanunoo"); + mappings[G_UNICODE_SCRIPT_BUHID] = _("Buhid"); + mappings[G_UNICODE_SCRIPT_TAGBANWA] = _("Tagbanwa"); + mappings[G_UNICODE_SCRIPT_BRAILLE] = _("Braille"); + mappings[G_UNICODE_SCRIPT_CYPRIOT] = _("Cypriot"); + mappings[G_UNICODE_SCRIPT_LIMBU] = _("Limbu"); + mappings[G_UNICODE_SCRIPT_OSMANYA] = _("Osmanya"); + mappings[G_UNICODE_SCRIPT_SHAVIAN] = _("Shavian"); + mappings[G_UNICODE_SCRIPT_LINEAR_B] = _("Linear B"); + mappings[G_UNICODE_SCRIPT_TAI_LE] = _("Tai Le"); + mappings[G_UNICODE_SCRIPT_UGARITIC] = _("Ugaritic"); + mappings[G_UNICODE_SCRIPT_NEW_TAI_LUE] = _("New Tai Lue"); + mappings[G_UNICODE_SCRIPT_BUGINESE] = _("Buginese"); + mappings[G_UNICODE_SCRIPT_GLAGOLITIC] = _("Glagolitic"); + mappings[G_UNICODE_SCRIPT_TIFINAGH] = _("Tifinagh"); + mappings[G_UNICODE_SCRIPT_SYLOTI_NAGRI] = _("Syloti Nagri"); + mappings[G_UNICODE_SCRIPT_OLD_PERSIAN] = _("Old Persian"); + mappings[G_UNICODE_SCRIPT_KHAROSHTHI] = _("Kharoshthi"); + mappings[G_UNICODE_SCRIPT_UNKNOWN] = _("unassigned"); + mappings[G_UNICODE_SCRIPT_BALINESE] = _("Balinese"); + mappings[G_UNICODE_SCRIPT_CUNEIFORM] = _("Cuneiform"); + mappings[G_UNICODE_SCRIPT_PHOENICIAN] = _("Phoenician"); + mappings[G_UNICODE_SCRIPT_PHAGS_PA] = _("Phags-pa"); + mappings[G_UNICODE_SCRIPT_NKO] = _("N'Ko"); + mappings[G_UNICODE_SCRIPT_KAYAH_LI] = _("Kayah Li"); + mappings[G_UNICODE_SCRIPT_LEPCHA] = _("Lepcha"); + mappings[G_UNICODE_SCRIPT_REJANG] = _("Rejang"); + mappings[G_UNICODE_SCRIPT_SUNDANESE] = _("Sundanese"); + mappings[G_UNICODE_SCRIPT_SAURASHTRA] = _("Saurashtra"); + mappings[G_UNICODE_SCRIPT_CHAM] = _("Cham"); + mappings[G_UNICODE_SCRIPT_OL_CHIKI] = _("Ol Chiki"); + mappings[G_UNICODE_SCRIPT_VAI] = _("Vai"); + mappings[G_UNICODE_SCRIPT_CARIAN] = _("Carian"); + mappings[G_UNICODE_SCRIPT_LYCIAN] = _("Lycian"); + mappings[G_UNICODE_SCRIPT_LYDIAN] = _("Lydian"); + mappings[G_UNICODE_SCRIPT_AVESTAN] = _("Avestan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BAMUM] = _("Bamum"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_EGYPTIAN_HIEROGLYPHS] = _("Egyptian Hieroglpyhs"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_IMPERIAL_ARAMAIC] = _("Imperial Aramaic"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PAHLAVI]= _("Inscriptional Pahlavi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PARTHIAN]= _("Inscriptional Parthian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_JAVANESE] = _("Javanese"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_KAITHI] = _("Kaithi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_LISU] = _("Lisu"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_MEETEI_MAYEK] = _("Meetei Mayek"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_SOUTH_ARABIAN] = _("Old South Arabian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_TURKIC] = _("Old Turkic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_SAMARITAN] = _("Samaritan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_THAM] = _("Tai Tham"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_VIET] = _("Tai Viet"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BATAK] = _("Batak"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_BRAHMI] = _("Brahmi"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_MANDAIC] = _("Mandaic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_CHAKMA] = _("Chakma"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_CURSIVE] = _("Meroitic Cursive"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS] = _("Meroitic Hieroglyphs"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MIAO] = _("Miao"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SHARADA] = _("Sharada"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SORA_SOMPENG] = _("Sora Sompeng"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_TAKRI] = _("Takri"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_BASSA_VAH] = _("Bassa"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_CAUCASIAN_ALBANIAN] = _("Caucasian Albanian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_DUPLOYAN] = _("Duployan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_ELBASAN] = _("Elbasan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_GRANTHA] = _("Grantha"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHOJKI] = _("Khojki"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHUDAWADI] = _("Khudawadi, Sindhi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_LINEAR_A] = _("Linear A"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MAHAJANI] = _("Mahajani"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MANICHAEAN] = _("Manichaean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MENDE_KIKAKUI] = _("Mende Kikakui"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MODI] = _("Modi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MRO] = _("Mro"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_NABATAEAN] = _("Nabataean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_NORTH_ARABIAN] = _("Old North Arabian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_PERMIC] = _("Old Permic"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAHAWH_HMONG] = _("Pahawh Hmong"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PALMYRENE] = _("Palmyrene"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAU_CIN_HAU] = _("Pau Cin Hau"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PSALTER_PAHLAVI] = _("Psalter Pahlavi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_SIDDHAM] = _("Siddham"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_TIRHUTA] = _("Tirhuta"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_WARANG_CITI] = _("Warang Citi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_AHOM] = _("Ahom"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_ANATOLIAN_HIEROGLYPHS]= _("Anatolian Hieroglyphs"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_HATRAN] = _("Hatran"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_MULTANI] = _("Multani"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_OLD_HUNGARIAN] = _("Old Hungarian"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_SIGNWRITING] = _("Signwriting"); // Since: 2.48 +/* + mappings[G_UNICODE_SCRIPT_ADLAM] = _("Adlam"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_BHAIKSUKI] = _("Bhaiksuki"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MARCHEN] = _("Marchen"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_NEWA] = _("Newa"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_OSAGE] = _("Osage"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_TANGUT] = _("Tangut"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MASARAM_GONDI] = _("Masaram Gondi"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_NUSHU] = _("Nushu"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_SOYOMBO] = _("Soyombo"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_ZANABAZAR_SQUARE] = _("Zanabazar Square"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_DOGRA] = _("Dogra"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_GUNJALA_GONDI] = _("Gunjala Gondi"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_HANIFI_ROHINGYA] = _("Hanifi Rohingya"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MAKASAR] = _("Makasar"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MEDEFAIDRIN] = _("Medefaidrin"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_OLD_SOGDIAN] = _("Old Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_SOGDIAN] = _("Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_ELYMAIC] = _("Elym"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NANDINAGARI] = _("Nand"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NYIAKENG_PUACHUE_HMONG]= _("Rohg"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_WANCHO] = _("Wcho"); // Since: 2.62 +*/ + } + return mappings; +} + +typedef std::pair Range; +typedef std::pair NamedRange; + +static std::vector & getRanges() +{ + static bool init = false; + static std::vector ranges; + if (!init) { + init = true; + ranges.emplace_back(std::make_pair(0x00000, 0x2FFFF), _("all")); + ranges.emplace_back(std::make_pair(0x00000, 0x0FFFF), _("Basic Plane")); + ranges.emplace_back(std::make_pair(0x10000, 0x1FFFF), _("Extended Multilingual Plane")); + ranges.emplace_back(std::make_pair(0x20000, 0x2FFFF), _("Supplementary Ideographic Plane")); + + ranges.emplace_back(std::make_pair(0x0000, 0x007F), _("Basic Latin")); + ranges.emplace_back(std::make_pair(0x0080, 0x00FF), _("Latin-1 Supplement")); + ranges.emplace_back(std::make_pair(0x0100, 0x017F), _("Latin Extended-A")); + ranges.emplace_back(std::make_pair(0x0180, 0x024F), _("Latin Extended-B")); + ranges.emplace_back(std::make_pair(0x0250, 0x02AF), _("IPA Extensions")); + ranges.emplace_back(std::make_pair(0x02B0, 0x02FF), _("Spacing Modifier Letters")); + ranges.emplace_back(std::make_pair(0x0300, 0x036F), _("Combining Diacritical Marks")); + ranges.emplace_back(std::make_pair(0x0370, 0x03FF), _("Greek and Coptic")); + ranges.emplace_back(std::make_pair(0x0400, 0x04FF), _("Cyrillic")); + ranges.emplace_back(std::make_pair(0x0500, 0x052F), _("Cyrillic Supplement")); + ranges.emplace_back(std::make_pair(0x0530, 0x058F), _("Armenian")); + ranges.emplace_back(std::make_pair(0x0590, 0x05FF), _("Hebrew")); + ranges.emplace_back(std::make_pair(0x0600, 0x06FF), _("Arabic")); + ranges.emplace_back(std::make_pair(0x0700, 0x074F), _("Syriac")); + ranges.emplace_back(std::make_pair(0x0750, 0x077F), _("Arabic Supplement")); + ranges.emplace_back(std::make_pair(0x0780, 0x07BF), _("Thaana")); + ranges.emplace_back(std::make_pair(0x07C0, 0x07FF), _("NKo")); + ranges.emplace_back(std::make_pair(0x0800, 0x083F), _("Samaritan")); + ranges.emplace_back(std::make_pair(0x0900, 0x097F), _("Devanagari")); + ranges.emplace_back(std::make_pair(0x0980, 0x09FF), _("Bengali")); + ranges.emplace_back(std::make_pair(0x0A00, 0x0A7F), _("Gurmukhi")); + ranges.emplace_back(std::make_pair(0x0A80, 0x0AFF), _("Gujarati")); + ranges.emplace_back(std::make_pair(0x0B00, 0x0B7F), _("Oriya")); + ranges.emplace_back(std::make_pair(0x0B80, 0x0BFF), _("Tamil")); + ranges.emplace_back(std::make_pair(0x0C00, 0x0C7F), _("Telugu")); + ranges.emplace_back(std::make_pair(0x0C80, 0x0CFF), _("Kannada")); + ranges.emplace_back(std::make_pair(0x0D00, 0x0D7F), _("Malayalam")); + ranges.emplace_back(std::make_pair(0x0D80, 0x0DFF), _("Sinhala")); + ranges.emplace_back(std::make_pair(0x0E00, 0x0E7F), _("Thai")); + ranges.emplace_back(std::make_pair(0x0E80, 0x0EFF), _("Lao")); + ranges.emplace_back(std::make_pair(0x0F00, 0x0FFF), _("Tibetan")); + ranges.emplace_back(std::make_pair(0x1000, 0x109F), _("Myanmar")); + ranges.emplace_back(std::make_pair(0x10A0, 0x10FF), _("Georgian")); + ranges.emplace_back(std::make_pair(0x1100, 0x11FF), _("Hangul Jamo")); + ranges.emplace_back(std::make_pair(0x1200, 0x137F), _("Ethiopic")); + ranges.emplace_back(std::make_pair(0x1380, 0x139F), _("Ethiopic Supplement")); + ranges.emplace_back(std::make_pair(0x13A0, 0x13FF), _("Cherokee")); + ranges.emplace_back(std::make_pair(0x1400, 0x167F), _("Unified Canadian Aboriginal Syllabics")); + ranges.emplace_back(std::make_pair(0x1680, 0x169F), _("Ogham")); + ranges.emplace_back(std::make_pair(0x16A0, 0x16FF), _("Runic")); + ranges.emplace_back(std::make_pair(0x1700, 0x171F), _("Tagalog")); + ranges.emplace_back(std::make_pair(0x1720, 0x173F), _("Hanunoo")); + ranges.emplace_back(std::make_pair(0x1740, 0x175F), _("Buhid")); + ranges.emplace_back(std::make_pair(0x1760, 0x177F), _("Tagbanwa")); + ranges.emplace_back(std::make_pair(0x1780, 0x17FF), _("Khmer")); + ranges.emplace_back(std::make_pair(0x1800, 0x18AF), _("Mongolian")); + ranges.emplace_back(std::make_pair(0x18B0, 0x18FF), _("Unified Canadian Aboriginal Syllabics Extended")); + ranges.emplace_back(std::make_pair(0x1900, 0x194F), _("Limbu")); + ranges.emplace_back(std::make_pair(0x1950, 0x197F), _("Tai Le")); + ranges.emplace_back(std::make_pair(0x1980, 0x19DF), _("New Tai Lue")); + ranges.emplace_back(std::make_pair(0x19E0, 0x19FF), _("Khmer Symbols")); + ranges.emplace_back(std::make_pair(0x1A00, 0x1A1F), _("Buginese")); + ranges.emplace_back(std::make_pair(0x1A20, 0x1AAF), _("Tai Tham")); + ranges.emplace_back(std::make_pair(0x1B00, 0x1B7F), _("Balinese")); + ranges.emplace_back(std::make_pair(0x1B80, 0x1BBF), _("Sundanese")); + ranges.emplace_back(std::make_pair(0x1C00, 0x1C4F), _("Lepcha")); + ranges.emplace_back(std::make_pair(0x1C50, 0x1C7F), _("Ol Chiki")); + ranges.emplace_back(std::make_pair(0x1CD0, 0x1CFF), _("Vedic Extensions")); + ranges.emplace_back(std::make_pair(0x1D00, 0x1D7F), _("Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x1D80, 0x1DBF), _("Phonetic Extensions Supplement")); + ranges.emplace_back(std::make_pair(0x1DC0, 0x1DFF), _("Combining Diacritical Marks Supplement")); + ranges.emplace_back(std::make_pair(0x1E00, 0x1EFF), _("Latin Extended Additional")); + ranges.emplace_back(std::make_pair(0x1F00, 0x1FFF), _("Greek Extended")); + ranges.emplace_back(std::make_pair(0x2000, 0x206F), _("General Punctuation")); + ranges.emplace_back(std::make_pair(0x2070, 0x209F), _("Superscripts and Subscripts")); + ranges.emplace_back(std::make_pair(0x20A0, 0x20CF), _("Currency Symbols")); + ranges.emplace_back(std::make_pair(0x20D0, 0x20FF), _("Combining Diacritical Marks for Symbols")); + ranges.emplace_back(std::make_pair(0x2100, 0x214F), _("Letterlike Symbols")); + ranges.emplace_back(std::make_pair(0x2150, 0x218F), _("Number Forms")); + ranges.emplace_back(std::make_pair(0x2190, 0x21FF), _("Arrows")); + ranges.emplace_back(std::make_pair(0x2200, 0x22FF), _("Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2300, 0x23FF), _("Miscellaneous Technical")); + ranges.emplace_back(std::make_pair(0x2400, 0x243F), _("Control Pictures")); + ranges.emplace_back(std::make_pair(0x2440, 0x245F), _("Optical Character Recognition")); + ranges.emplace_back(std::make_pair(0x2460, 0x24FF), _("Enclosed Alphanumerics")); + ranges.emplace_back(std::make_pair(0x2500, 0x257F), _("Box Drawing")); + ranges.emplace_back(std::make_pair(0x2580, 0x259F), _("Block Elements")); + ranges.emplace_back(std::make_pair(0x25A0, 0x25FF), _("Geometric Shapes")); + ranges.emplace_back(std::make_pair(0x2600, 0x26FF), _("Miscellaneous Symbols")); + ranges.emplace_back(std::make_pair(0x2700, 0x27BF), _("Dingbats")); + ranges.emplace_back(std::make_pair(0x27C0, 0x27EF), _("Miscellaneous Mathematical Symbols-A")); + ranges.emplace_back(std::make_pair(0x27F0, 0x27FF), _("Supplemental Arrows-A")); + ranges.emplace_back(std::make_pair(0x2800, 0x28FF), _("Braille Patterns")); + ranges.emplace_back(std::make_pair(0x2900, 0x297F), _("Supplemental Arrows-B")); + ranges.emplace_back(std::make_pair(0x2980, 0x29FF), _("Miscellaneous Mathematical Symbols-B")); + ranges.emplace_back(std::make_pair(0x2A00, 0x2AFF), _("Supplemental Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2B00, 0x2BFF), _("Miscellaneous Symbols and Arrows")); + ranges.emplace_back(std::make_pair(0x2C00, 0x2C5F), _("Glagolitic")); + ranges.emplace_back(std::make_pair(0x2C60, 0x2C7F), _("Latin Extended-C")); + ranges.emplace_back(std::make_pair(0x2C80, 0x2CFF), _("Coptic")); + ranges.emplace_back(std::make_pair(0x2D00, 0x2D2F), _("Georgian Supplement")); + ranges.emplace_back(std::make_pair(0x2D30, 0x2D7F), _("Tifinagh")); + ranges.emplace_back(std::make_pair(0x2D80, 0x2DDF), _("Ethiopic Extended")); + ranges.emplace_back(std::make_pair(0x2DE0, 0x2DFF), _("Cyrillic Extended-A")); + ranges.emplace_back(std::make_pair(0x2E00, 0x2E7F), _("Supplemental Punctuation")); + ranges.emplace_back(std::make_pair(0x2E80, 0x2EFF), _("CJK Radicals Supplement")); + ranges.emplace_back(std::make_pair(0x2F00, 0x2FDF), _("Kangxi Radicals")); + ranges.emplace_back(std::make_pair(0x2FF0, 0x2FFF), _("Ideographic Description Characters")); + ranges.emplace_back(std::make_pair(0x3000, 0x303F), _("CJK Symbols and Punctuation")); + ranges.emplace_back(std::make_pair(0x3040, 0x309F), _("Hiragana")); + ranges.emplace_back(std::make_pair(0x30A0, 0x30FF), _("Katakana")); + ranges.emplace_back(std::make_pair(0x3100, 0x312F), _("Bopomofo")); + ranges.emplace_back(std::make_pair(0x3130, 0x318F), _("Hangul Compatibility Jamo")); + ranges.emplace_back(std::make_pair(0x3190, 0x319F), _("Kanbun")); + ranges.emplace_back(std::make_pair(0x31A0, 0x31BF), _("Bopomofo Extended")); + ranges.emplace_back(std::make_pair(0x31C0, 0x31EF), _("CJK Strokes")); + ranges.emplace_back(std::make_pair(0x31F0, 0x31FF), _("Katakana Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x3200, 0x32FF), _("Enclosed CJK Letters and Months")); + ranges.emplace_back(std::make_pair(0x3300, 0x33FF), _("CJK Compatibility")); + ranges.emplace_back(std::make_pair(0x3400, 0x4DBF), _("CJK Unified Ideographs Extension A")); + ranges.emplace_back(std::make_pair(0x4DC0, 0x4DFF), _("Yijing Hexagram Symbols")); + ranges.emplace_back(std::make_pair(0x4E00, 0x9FFF), _("CJK Unified Ideographs")); + ranges.emplace_back(std::make_pair(0xA000, 0xA48F), _("Yi Syllables")); + ranges.emplace_back(std::make_pair(0xA490, 0xA4CF), _("Yi Radicals")); + ranges.emplace_back(std::make_pair(0xA4D0, 0xA4FF), _("Lisu")); + ranges.emplace_back(std::make_pair(0xA500, 0xA63F), _("Vai")); + ranges.emplace_back(std::make_pair(0xA640, 0xA69F), _("Cyrillic Extended-B")); + ranges.emplace_back(std::make_pair(0xA6A0, 0xA6FF), _("Bamum")); + ranges.emplace_back(std::make_pair(0xA700, 0xA71F), _("Modifier Tone Letters")); + ranges.emplace_back(std::make_pair(0xA720, 0xA7FF), _("Latin Extended-D")); + ranges.emplace_back(std::make_pair(0xA800, 0xA82F), _("Syloti Nagri")); + ranges.emplace_back(std::make_pair(0xA830, 0xA83F), _("Common Indic Number Forms")); + ranges.emplace_back(std::make_pair(0xA840, 0xA87F), _("Phags-pa")); + ranges.emplace_back(std::make_pair(0xA880, 0xA8DF), _("Saurashtra")); + ranges.emplace_back(std::make_pair(0xA8E0, 0xA8FF), _("Devanagari Extended")); + ranges.emplace_back(std::make_pair(0xA900, 0xA92F), _("Kayah Li")); + ranges.emplace_back(std::make_pair(0xA930, 0xA95F), _("Rejang")); + ranges.emplace_back(std::make_pair(0xA960, 0xA97F), _("Hangul Jamo Extended-A")); + ranges.emplace_back(std::make_pair(0xA980, 0xA9DF), _("Javanese")); + ranges.emplace_back(std::make_pair(0xAA00, 0xAA5F), _("Cham")); + ranges.emplace_back(std::make_pair(0xAA60, 0xAA7F), _("Myanmar Extended-A")); + ranges.emplace_back(std::make_pair(0xAA80, 0xAADF), _("Tai Viet")); + ranges.emplace_back(std::make_pair(0xABC0, 0xABFF), _("Meetei Mayek")); + ranges.emplace_back(std::make_pair(0xAC00, 0xD7AF), _("Hangul Syllables")); + ranges.emplace_back(std::make_pair(0xD7B0, 0xD7FF), _("Hangul Jamo Extended-B")); + ranges.emplace_back(std::make_pair(0xD800, 0xDB7F), _("High Surrogates")); + ranges.emplace_back(std::make_pair(0xDB80, 0xDBFF), _("High Private Use Surrogates")); + ranges.emplace_back(std::make_pair(0xDC00, 0xDFFF), _("Low Surrogates")); + ranges.emplace_back(std::make_pair(0xE000, 0xF8FF), _("Private Use Area")); + ranges.emplace_back(std::make_pair(0xF900, 0xFAFF), _("CJK Compatibility Ideographs")); + ranges.emplace_back(std::make_pair(0xFB00, 0xFB4F), _("Alphabetic Presentation Forms")); + ranges.emplace_back(std::make_pair(0xFB50, 0xFDFF), _("Arabic Presentation Forms-A")); + ranges.emplace_back(std::make_pair(0xFE00, 0xFE0F), _("Variation Selectors")); + ranges.emplace_back(std::make_pair(0xFE10, 0xFE1F), _("Vertical Forms")); + ranges.emplace_back(std::make_pair(0xFE20, 0xFE2F), _("Combining Half Marks")); + ranges.emplace_back(std::make_pair(0xFE30, 0xFE4F), _("CJK Compatibility Forms")); + ranges.emplace_back(std::make_pair(0xFE50, 0xFE6F), _("Small Form Variants")); + ranges.emplace_back(std::make_pair(0xFE70, 0xFEFF), _("Arabic Presentation Forms-B")); + ranges.emplace_back(std::make_pair(0xFF00, 0xFFEF), _("Halfwidth and Fullwidth Forms")); + ranges.emplace_back(std::make_pair(0xFFF0, 0xFFFF), _("Specials")); + + // Selected ranges in Extended Multilingual Plane + ranges.emplace_back(std::make_pair(0x1F300, 0x1F5FF), _("Miscellaneous Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1F600, 0x1F64F), _("Emoticons")); + ranges.emplace_back(std::make_pair(0x1F650, 0x1F67F), _("Ornamental Dingbats")); + ranges.emplace_back(std::make_pair(0x1F680, 0x1F6FF), _("Transport and Map Symbols")); + ranges.emplace_back(std::make_pair(0x1F700, 0x1F77F), _("Alchemical Symbols")); + ranges.emplace_back(std::make_pair(0x1F780, 0x1F7FF), _("Geometric Shapes Extended")); + ranges.emplace_back(std::make_pair(0x1F800, 0x1F8FF), _("Supplemental Arrows-C")); + ranges.emplace_back(std::make_pair(0x1F900, 0x1F9FF), _("Supplemental Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1FA00, 0x1FA7F), _("Chess Symbols")); + ranges.emplace_back(std::make_pair(0x1FA80, 0x1FAFF), _("Symbols and Pictographs Extended-A")); + + } + + return ranges; +} + +class GlyphColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn code; + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn tooltip; + + GlyphColumns() + { + add(code); + add(name); + add(tooltip); + } +}; + +GlyphColumns *GlyphsPanel::getColumns() +{ + static GlyphColumns *columns = new GlyphColumns(); + + return columns; +} + +/** + * Constructor + */ +GlyphsPanel::GlyphsPanel() : + Inkscape::UI::Widget::Panel("/dialogs/glyphs", SP_VERB_DIALOG_GLYPHS), + store(Gtk::ListStore::create(*getColumns())), + deskTrack(), + instanceConns(), + desktopConns() +{ + auto table = new Gtk::Grid(); + table->set_row_spacing(4); + table->set_column_spacing(4); + _getContents()->pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET); + guint row = 0; + +// ------------------------------- + + { + fontSelector = new Inkscape::UI::Widget::FontSelector (false, false); + fontSelector->set_name ("UnicodeCharacters"); + + sigc::connection conn = + fontSelector->connectChanged(sigc::hide(sigc::mem_fun(*this, &GlyphsPanel::rebuild))); + instanceConns.push_back(conn); + + table->attach(*Gtk::manage(fontSelector), 0, row, 3, 1); + row++; + } + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Script: ")); + + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + scriptCombo = Gtk::manage(new Gtk::ComboBoxText()); + for (auto & it : getScriptToName()) + { + scriptCombo->append(it.second); + } + + scriptCombo->set_active_text(getScriptToName()[G_UNICODE_SCRIPT_INVALID_CODE]); + sigc::connection conn = scriptCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + scriptCombo->set_halign(Gtk::ALIGN_START); + scriptCombo->set_valign(Gtk::ALIGN_START); + scriptCombo->set_hexpand(); + table->attach(*scriptCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Range: ")); + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + rangeCombo = Gtk::manage(new Gtk::ComboBoxText()); + for (auto & it : getRanges()) { + rangeCombo->append(it.second); + } + + rangeCombo->set_active_text(getRanges()[4].second); + sigc::connection conn = rangeCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + rangeCombo->set_halign(Gtk::ALIGN_START); + rangeCombo->set_valign(Gtk::ALIGN_START); + rangeCombo->set_hexpand(); + table->attach(*rangeCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + GlyphColumns *columns = getColumns(); + + iconView = new Gtk::IconView(static_cast >(store)); + iconView->set_name("UnicodeIconView"); + iconView->set_markup_column(columns->name); + iconView->set_tooltip_column(2); // Uses Pango markup, must use column number. + iconView->set_margin(0); + iconView->set_item_padding(0); + iconView->set_row_spacing(0); + iconView->set_column_spacing(0); + + sigc::connection conn; + conn = iconView->signal_item_activated().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphActivated)); + instanceConns.push_back(conn); + conn = iconView->signal_selection_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphSelectionChanged)); + instanceConns.push_back(conn); + + + Gtk::ScrolledWindow *scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->add(*Gtk::manage(iconView)); + scroller->set_hexpand(); + scroller->set_vexpand(); + table->attach(*Gtk::manage(scroller), 0, row, 3, 1); + + row++; + +// ------------------------------- + + Gtk::HBox *box = new Gtk::HBox(); + + entry = new Gtk::Entry(); + conn = entry->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::calcCanInsert)); + instanceConns.push_back(conn); + entry->set_width_chars(18); + box->pack_start(*Gtk::manage(entry), Gtk::PACK_SHRINK); + + Gtk::Label *pad = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK); + + label = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(label), Gtk::PACK_SHRINK); + + pad = new Gtk::Label(""); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET); + + insertBtn = new Gtk::Button(_("Append")); + conn = insertBtn->signal_clicked().connect(sigc::mem_fun(*this, &GlyphsPanel::insertText)); + instanceConns.push_back(conn); + insertBtn->set_can_default(); + insertBtn->set_sensitive(false); + + box->pack_end(*Gtk::manage(insertBtn), Gtk::PACK_SHRINK); + box->set_hexpand(); + table->attach( *Gtk::manage(box), 0, row, 3, 1); + + row++; + +// ------------------------------- + + + show_all_children(); + + // Connect this up last + conn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &GlyphsPanel::setTargetDesktop) ); + instanceConns.push_back(conn); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +GlyphsPanel::~GlyphsPanel() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + instanceConns.clear(); + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); +} + + +void GlyphsPanel::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void GlyphsPanel::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + if (targetDesktop) { + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); + } + + targetDesktop = desktop; + + if (targetDesktop && targetDesktop->selection) { + sigc::connection conn = desktop->selection->connectChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, true))); + desktopConns.push_back(conn); + + // Text selection within selected items has changed: + conn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, false))); + desktopConns.push_back(conn); + + // Must check flags, so can't call performUpdate() directly. + conn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &GlyphsPanel::selectionModifiedCB))); + desktopConns.push_back(conn); + + readSelection(true, true); + } + } +} + +// Append selected glyphs to selected text +void GlyphsPanel::insertText() +{ + SPItem *textItem = nullptr; + auto itemlist= targetDesktop->selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + textItem = *i; + break; + } + } + + if (textItem) { + Glib::ustring glyphs; + if (entry->get_text_length() > 0) { + glyphs = entry->get_text(); + } else { + auto itemArray = iconView->get_selected_items(); + + if (!itemArray.empty()) { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + glyphs = ch; + } + } + + if (!glyphs.empty()) { + Glib::ustring combined; + gchar *str = sp_te_get_string_multiline(textItem); + if (str) { + combined = str; + g_free(str); + str = nullptr; + } + combined += glyphs; + sp_te_set_repr_text_multiline(textItem, combined.c_str()); + DocumentUndo::done(targetDesktop->doc(), SP_VERB_CONTEXT_TEXT, _("Append text")); + } + } +} + +void GlyphsPanel::glyphActivated(Gtk::TreeModel::Path const & path) +{ + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + Glib::ustring tmp; + tmp += ch; + + int startPos = 0; + int endPos = 0; + if (entry->get_selection_bounds(startPos, endPos)) { + // there was something selected. + entry->delete_text(startPos, endPos); + } + startPos = entry->get_position(); + entry->insert_text(tmp, -1, startPos); + entry->set_position(startPos); +} + +void GlyphsPanel::glyphSelectionChanged() +{ + auto itemArray = iconView->get_selected_items(); + + if (itemArray.empty()) { + label->set_text(" "); + } else { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + + + Glib::ustring scriptName; + GUnicodeScript script = g_unichar_get_script(ch); + std::map mappings = getScriptToName(); + if (mappings.find(script) != mappings.end()) { + scriptName = mappings[script]; + } + gchar * tmp = g_strdup_printf("U+%04X %s", ch, scriptName.c_str()); + label->set_text(tmp); + } + calcCanInsert(); +} + +void GlyphsPanel::selectionModifiedCB(guint flags) +{ + bool style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + + bool content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + + readSelection(style, content); +} + +void GlyphsPanel::calcCanInsert() +{ + int items = 0; + auto itemlist= targetDesktop->selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + ++items; + } + } + + bool enable = (items == 1); + if (enable) { + enable &= (!iconView->get_selected_items().empty() + || (entry->get_text_length() > 0)); + } + + if (enable != insertBtn->is_sensitive()) { + insertBtn->set_sensitive(enable); + } +} + +void GlyphsPanel::readSelection( bool updateStyle, bool updateContent ) +{ + calcCanInsert(); + + if (updateStyle) { + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + + // Update family/style based on selection. + fontlister->selection_update(); + + // Update GUI (based on fontlister values). + fontSelector->update_font (); + } +} + + +void GlyphsPanel::rebuild() +{ + Glib::ustring fontspec = fontSelector->get_fontspec(); + + font_instance* font = nullptr; + if( !fontspec.empty() ) { + font = font_factory::Default()->FaceFromFontSpecification( fontspec.c_str() ); + } + + if (font) { + + GUnicodeScript script = G_UNICODE_SCRIPT_INVALID_CODE; + Glib::ustring scriptName = scriptCombo->get_active_text(); + std::map items = getScriptToName(); + for (auto & item : items) { + if (scriptName == item.second) { + script = item.first; + break; + } + } + + // Disconnect the model while we update it. Simple work-around for 5x+ performance boost. + Glib::RefPtr tmp = Gtk::ListStore::create(*getColumns()); + iconView->set_model(tmp); + + gunichar lower = 0x00001; + gunichar upper = 0x2FFFF; + int active = rangeCombo->get_active_row_number(); + if (active >= 0) { + lower = getRanges()[active].first.first; + upper = getRanges()[active].first.second; + } + std::vector present; + for (gunichar ch = lower; ch <= upper; ch++) { + int glyphId = font->MapUnicodeChar(ch); + if (glyphId > 0) { + if ((script == G_UNICODE_SCRIPT_INVALID_CODE) || (script == g_unichar_get_script(ch))) { + present.push_back(ch); + } + } + } + + GlyphColumns *columns = getColumns(); + store->clear(); + for (unsigned int & it : present) + { + Gtk::ListStore::iterator row = store->append(); + Glib::ustring tmp; + tmp += it; + tmp = Glib::Markup::escape_text(tmp); // Escape '&', '<', etc. + (*row)[columns->code] = it; + (*row)[columns->name] = "" + tmp + ""; + (*row)[columns->tooltip] = "" + tmp + ""; + } + + // Reconnect the model once it has been updated: + iconView->set_model(store); + } +} + + +} // namespace Dialogs +} // 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 : diff --git a/src/ui/dialog/glyphs.h b/src/ui/dialog/glyphs.h new file mode 100644 index 0000000..3307683 --- /dev/null +++ b/src/ui/dialog/glyphs.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOGS_GLYPHS_H +#define SEEN_DIALOGS_GLYPHS_H + +#include "ui/widget/panel.h" +#include +#include "ui/dialog/desktop-tracker.h" + + +namespace Gtk { +class ComboBoxText; +class Entry; +class IconView; +class Label; +class ListStore; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { +class FontSelector; +} + +namespace Dialog { + +class GlyphColumns; + +/** + * A panel that displays character glyphs. + */ +class GlyphsPanel : public Inkscape::UI::Widget::Panel +{ +public: + GlyphsPanel(); + ~GlyphsPanel() override; + + static GlyphsPanel& getInstance(); + + void setDesktop(SPDesktop *desktop) override; + +protected: + +private: + GlyphsPanel(GlyphsPanel const &) = delete; // no copy + GlyphsPanel &operator=(GlyphsPanel const &) = delete; // no assign + + static GlyphColumns *getColumns(); + + void rebuild(); + + void glyphActivated(Gtk::TreeModel::Path const & path); + void glyphSelectionChanged(); + void setTargetDesktop(SPDesktop *desktop); + void selectionModifiedCB(guint flags); + void readSelection( bool updateStyle, bool updateContent ); + void calcCanInsert(); + void insertText(); + + + Glib::RefPtr store; + Gtk::IconView *iconView; + Gtk::Entry *entry; + Gtk::Label *label; + Gtk::Button *insertBtn; + Gtk::ComboBoxText *scriptCombo; + Gtk::ComboBoxText *rangeCombo; + Inkscape::UI::Widget::FontSelector *fontSelector; + SPDesktop *targetDesktop; + DesktopTracker deskTrack; + + std::vector instanceConns; + std::vector desktopConns; +}; + + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_GLYPHS_H +/* + 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 : diff --git a/src/ui/dialog/grid-arrange-tab.cpp b/src/ui/dialog/grid-arrange-tab.cpp new file mode 100644 index 0000000..81baf78 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.cpp @@ -0,0 +1,776 @@ +// 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 <2geom/transforms.h> + +#include "verbs.h" +#include "preferences.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" +//#include "sp-item-transform.h" FIXME +#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog + + /* + * Sort items by their x co-ordinates, taking account of y (keeps rows intact) + * + * <0 *elem1 goes before *elem2 + * 0 *elem1 == *elem2 + * >0 *elem1 goes after *elem2 + */ + static bool sp_compare_x_position(SPItem *first, SPItem *second) + { + using Geom::X; + using Geom::Y; + + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + double const a_height = a->dimensions()[Y]; + double const b_height = b->dimensions()[Y]; + + bool a_in_b_vert = false; + if ((a->min()[Y] < b->min()[Y] + 0.1) && (a->min()[Y] > b->min()[Y] - b_height)) { + a_in_b_vert = true; + } else if ((b->min()[Y] < a->min()[Y] + 0.1) && (b->min()[Y] > a->min()[Y] - a_height)) { + a_in_b_vert = true; + } else if (b->min()[Y] == a->min()[Y]) { + a_in_b_vert = true; + } else { + a_in_b_vert = false; + } + + if (!a_in_b_vert) { // a and b are not in the same row + return (a->min()[Y] < b->min()[Y]); + } + return (a->min()[X] < b->min()[X]); + } + + /* + * Sort items by their y co-ordinates. + */ + static bool sp_compare_y_position(SPItem *first, SPItem *second) + { + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + if (a->min()[Geom::Y] > b->min()[Geom::Y]) { + return false; + } + if (a->min()[Geom::Y] < b->min()[Geom::Y]) { + return true; + } + + return false; + } + + + namespace Inkscape { + namespace UI { + namespace Dialog { + + + //######################################################################### + //## E V E N T S + //######################################################################### + + /* + * + * This arranges the selection in a grid pattern. + * + */ + + void GridArrangeTab::arrange() + { + + int cnt,row_cnt,col_cnt,a,row,col; + double grid_left,grid_top,col_width,row_height,paddingx,paddingy,width, height, new_x, new_y; + double total_col_width,total_row_height; + col_width = 0; + row_height = 0; + total_col_width=0; + total_row_height=0; + + // check for correct numbers in the row- and col-spinners + on_col_spinbutton_changed(); + on_row_spinbutton_changed(); + + // set padding to manual values + paddingx = XPadding.getValue("px"); + paddingy = YPadding.getValue("px"); + + std::vector row_heights; + std::vector col_widths; + std::vector row_ys; + std::vector col_xs; + + int NoOfCols = NoOfColsSpinner.get_value_as_int(); + int NoOfRows = NoOfRowsSpinner.get_value_as_int(); + + width = 0; + for (a=0;agetDesktop(); + desktop->getDocument()->ensureUpToDate(); + + Inkscape::Selection *selection = desktop->getSelection(); + std::vector items; + if (selection) { + items.insert(items.end(), selection->items().begin(), selection->items().end()); + } + + for(auto item : items){ + Geom::OptRect b = item->documentVisualBounds(); + if (!b) { + continue; + } + + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + + if (b->min()[Geom::X] < grid_left) { + grid_left = b->min()[Geom::X]; + } + if (b->min()[Geom::Y] < grid_top) { + grid_top = b->min()[Geom::Y]; + } + if (width > col_width) { + col_width = width; + } + if (height > row_height) { + row_height = height; + } + } + + + // require the sorting done before we can calculate row heights etc. + + g_return_if_fail(selection); + std::vector sorted(selection->items().begin(), selection->items().end()); + sort(sorted.begin(),sorted.end(),sp_compare_y_position); + sort(sorted.begin(),sorted.end(),sp_compare_x_position); + + + // Calculate individual Row and Column sizes if necessary + + + cnt=0; + const std::vector sizes(sorted); + for (auto item : sizes) { + Geom::OptRect b = item->documentVisualBounds(); + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + if (width > col_widths[(cnt % NoOfCols)]) { + col_widths[(cnt % NoOfCols)] = width; + } + if (height > row_heights[(cnt / NoOfCols)]) { + row_heights[(cnt / NoOfCols)] = height; + } + } + + cnt++; + } + + + /// 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)); + } + + #ifdef DEBUG_GRID_ARRANGE + g_print("\n cx = %f cy= %f gridleft=%f",cx,cy,grid_left); + #endif + + // Calculate total widths and heights, allowing for columns and rows non uniformly sized. + + if (ColumnWidthButton.get_active()){ + total_col_width = col_width * NoOfCols; + col_widths.clear(); + for (a=0;avisualBounds(); + // Fit to bbox, calculate padding between rows accordingly. + if ( sel_bbox && !SpaceManualRadioButton.get_active() ){ +#ifdef DEBUG_GRID_ARRANGE +g_print("\n row = %f col = %f selection x= %f selection y = %f", total_row_height,total_col_width, b.extent(Geom::X), b.extent(Geom::Y)); +#endif + paddingx = (sel_bbox->width() - total_col_width) / (NoOfCols -1); + paddingy = (sel_bbox->height() - total_row_height) / (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. + + for (a=0;a::iterator it = sorted.begin(); + for (row_cnt=0; ((it != sorted.end()) && (row_cnt current_row; + col_cnt = 0; + for(;it!=sorted.end()&&col_cntgetRepr(); + Geom::OptRect b = item->documentVisualBounds(); + Geom::Point min; + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + min = b->min(); + } else { + width = height = 0; + min = Geom::Point(0, 0); + } + + row = cnt / NoOfCols; + col = cnt % NoOfCols; + + new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col]; + 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(), SP_VERB_SELECTION_ARRANGE, + _("Arrange in a grid")); + +} + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * changed value in # of columns spinbox. + */ +void GridArrangeTab::on_row_spinbutton_changed() +{ + // 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->selection : nullptr; + g_return_if_fail( selection ); + + int selcount = (int) boost::distance(selection->items()); + + double PerCol = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(PerCol); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", NoOfColsSpinner.get_value()); + updating=false; +} + +/** + * changed value in # of rows spinbox. + */ +void GridArrangeTab::on_col_spinbutton_changed() +{ + // 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->selection : nullptr; + g_return_if_fail(selection); + + int selcount = (int) boost::distance(selection->items()); + + double PerRow = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(PerRow); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", PerRow); + + updating=false; +} + +/** + * 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; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // in turn, prevent listener from responding + updating = true; + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : 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); + prefs->setInt("/dialogs/gridtiler/NoOfCols", NoOfCols); + } + } else { + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + NoOfRowsSpinner.set_value(PerRow); + NoOfColsSpinner.set_value(PerCol); + prefs->setInt("/dialogs/gridtiler/NoOfCols", static_cast(PerCol)); + } + } + + updating = false; +} + + +//######################################################################### +//## 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(); + + // could not do this in gtkmm - there's no Gtk::SizeGroup public constructor (!) + GtkSizeGroup *_col1 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col2 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col3 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + { + // Selection Change signal + INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection))); + } + + Gtk::Box *contents = this; + +#define MARGIN 2 + + //##Set up the panel + + SPDesktop *desktop = Parent->getDesktop(); + + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + g_return_if_fail( selection ); + int selcount = 1; + if (!selection->isEmpty()) { + selcount = (int) boost::distance(selection->items()); + } + + + /*#### Number of Rows ####*/ + + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + + #ifdef DEBUG_GRID_ARRANGE + g_print("/n PerRox = %f PerCol = %f selcount = %d",PerRow,PerCol,selcount); + #endif + + 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.set_value(PerCol); + 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); + gtk_size_group_add_widget(_col1, (GtkWidget *) NoOfRowsBox.gobj()); + + 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); + gtk_size_group_add_widget(_col2, GTK_WIDGET(XByYLabelVBox.gobj())); + + /*#### 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.set_value(PerRow); + 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); + gtk_size_group_add_widget(_col3, GTK_WIDGET(NoOfColsBox.gobj())); + + 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); + + //## The OK button FIXME + /*TileOkButton = addResponseButton(C_("Rows and columns dialog","_Arrange"), GTK_RESPONSE_APPLY); + TileOkButton->set_use_underline(true); + TileOkButton->set_tooltip_text(_("Arrange selected objects"));*/ + + show_all_children(); +} + +} //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 : diff --git a/src/ui/dialog/grid-arrange-tab.h b/src/ui/dialog/grid-arrange-tab.h new file mode 100644 index 0000000..ff6afb8 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Grid + */ +/* 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. + */ + +#ifndef INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/dialog/arrange-tab.h" + +#include "ui/widget/anchor-selector.h" +#include "ui/widget/spinbutton.h" + +#include +#include +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * Dialog for tiling an object + */ +class GridArrangeTab : public ArrangeTab { +public: + GridArrangeTab(ArrangeDialog *parent); + ~GridArrangeTab() override = default;; + + /** + * Do the actual work + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + // Callbacks from spinbuttons + void on_row_spinbutton_changed(); + void on_col_spinbutton_changed(); + void on_xpad_spinbutton_changed(); + void on_ypad_spinbutton_changed(); + void on_RowSize_checkbutton_changed(); + void on_ColSize_checkbutton_changed(); + void on_rowSize_spinbutton_changed(); + void on_colSize_spinbutton_changed(); + void Spacing_button_changed(); + void Align_changed(); + + +private: + GridArrangeTab(GridArrangeTab const &d) = delete; // no copy + void operator=(GridArrangeTab const &d) = delete; // no assign + + ArrangeDialog *Parent; + + bool userHidden; + bool updating; + + Gtk::Box TileBox; + Gtk::Button *TileOkButton; + Gtk::Button *TileCancelButton; + + // Number selected label + Gtk::Label SelectionContentsLabel; + + + Gtk::Box AlignHBox; + Gtk::Box SpinsHBox; + + // Number per Row + Gtk::Box NoOfColsBox; + Gtk::Label NoOfColsLabel; + Inkscape::UI::Widget::SpinButton NoOfColsSpinner; + bool AutoRowSize; + Gtk::CheckButton RowHeightButton; + + Gtk::Box XByYLabelVBox; + Gtk::Label padXByYLabel; + Gtk::Label XByYLabel; + + // Number per Column + Gtk::Box NoOfRowsBox; + Gtk::Label NoOfRowsLabel; + Inkscape::UI::Widget::SpinButton NoOfRowsSpinner; + bool AutoColSize; + Gtk::CheckButton ColumnWidthButton; + + // Alignment + Gtk::Label AlignLabel; + Inkscape::UI::Widget::AnchorSelector AlignmentSelector; + double VertAlign; + double HorizAlign; + + Inkscape::UI::Widget::UnitMenu PaddingUnitMenu; + Inkscape::UI::Widget::ScalarUnit XPadding; + Inkscape::UI::Widget::ScalarUnit YPadding; + Gtk::Grid *PaddingTable; + + // BBox or manual spacing + Gtk::VBox SpacingVBox; + Gtk::RadioButtonGroup SpacingGroup; + Gtk::RadioButton SpaceByBBoxRadioButton; + Gtk::RadioButton SpaceManualRadioButton; + bool ManualSpacing; + + // Row height + Gtk::Box RowHeightBox; + Inkscape::UI::Widget::SpinButton RowHeightSpinner; + + // Column width + Gtk::Box ColumnWidthBox; + Inkscape::UI::Widget::SpinButton ColumnWidthSpinner; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H */ + +/* + 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 : diff --git a/src/ui/dialog/guides.cpp b/src/ui/dialog/guides.cpp new file mode 100644 index 0000000..03743b8 --- /dev/null +++ b/src/ui/dialog/guides.cpp @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Simple guideline dialog. + */ +/* Authors: + * Lauris Kaplinski + * Andrius R. + * Johan Engelen + * Abhishek Sharma + * + * Copyright (C) 1999-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "guides.h" + +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +#include "display/guideline.h" + +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.h" + +#include "widgets/desktop-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +GuidelinePropertiesDialog::GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop) +: _desktop(desktop), _guide(guide), + _locked_toggle(_("Lo_cked")), + _relative_toggle(_("Rela_tive change")), + _spin_button_x(C_("Guides", "_X:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _spin_button_y(C_("Guides", "_Y:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _label_entry(_("_Label:"), _("Optionally give this guideline a name")), + _spin_angle(_("_Angle:"), "", UNIT_TYPE_RADIAL), + _mode(true), _oldpos(0.,0.), _oldangle(0.0) +{ + _locked_toggle.set_use_underline(); + _locked_toggle.set_tooltip_text(_("Lock the movement of guides")); + _relative_toggle.set_use_underline(); + _relative_toggle.set_tooltip_text(_("Move and/or rotate the guide relative to current settings")); +} + +bool GuidelinePropertiesDialog::_relative_toggle_status = false; // initialize relative checkbox status for when this dialog is opened for first time +Glib::ustring GuidelinePropertiesDialog::_angle_unit_status = DEG; // initialize angle unit status + +GuidelinePropertiesDialog::~GuidelinePropertiesDialog() { + // save current status + _relative_toggle_status = _relative_toggle.get_active(); + _angle_unit_status = _spin_angle.getUnit()->abbr; +} + +void GuidelinePropertiesDialog::showDialog(SPGuide *guide, SPDesktop *desktop) { + GuidelinePropertiesDialog dialog(guide, desktop); + dialog._setup(); + dialog.run(); +} + +void GuidelinePropertiesDialog::_modeChanged() +{ + _mode = !_relative_toggle.get_active(); + if (!_mode) { + // relative + _spin_angle.setValue(0); + + _spin_button_y.setValue(0); + _spin_button_x.setValue(0); + } else { + // absolute + _spin_angle.setValueKeepUnit(_oldangle, DEG); + + _spin_button_x.setValueKeepUnit(_oldpos[Geom::X], "px"); + _spin_button_y.setValueKeepUnit(_oldpos[Geom::Y], "px"); + } +} + +void GuidelinePropertiesDialog::_onOK() +{ + double deg_angle = _spin_angle.getValue(DEG); + if (!_mode) + deg_angle += _oldangle; + Geom::Point normal; + if ( deg_angle == 90. || deg_angle == 270. || deg_angle == -90. || deg_angle == -270.) { + normal = Geom::Point(1.,0.); + } else if ( deg_angle == 0. || deg_angle == 180. || deg_angle == -180.) { + normal = Geom::Point(0.,1.); + } else { + double rad_angle = Geom::rad_from_deg( deg_angle ); + normal = Geom::rot90(Geom::Point::polar(rad_angle, 1.0)); + } + //To allow reposition from dialog + _guide->set_locked(false, false); + + _guide->set_normal(normal, true); + + double const points_x = _spin_button_x.getValue("px"); + double const points_y = _spin_button_y.getValue("px"); + Geom::Point newpos(points_x, points_y); + if (!_mode) + newpos += _oldpos; + + _guide->moveto(newpos, true); + + const gchar* name = g_strdup( _label_entry.getEntry()->get_text().c_str() ); + + _guide->set_label(name, true); + + const bool locked = _locked_toggle.get_active(); + + _guide->set_locked(locked, true); + + g_free((gpointer) name); + + const auto c = _color.get_rgba(); + unsigned r = c.get_red_u()/257, g = c.get_green_u()/257, b = c.get_blue_u()/257; + //TODO: why 257? verify this! + // don't know why, but introduced: 761f7da58cd6d625b88c24eee6fae1b7fa3bfcdd + + _guide->set_color(r, g, b, true); + + DocumentUndo::done(_guide->document, SP_VERB_NONE, + _("Set guide properties")); +} + +void GuidelinePropertiesDialog::_onDelete() +{ + SPDocument *doc = _guide->document; + sp_guide_remove(_guide); + DocumentUndo::done(doc, SP_VERB_NONE, + _("Delete guide")); +} + +void GuidelinePropertiesDialog::_onDuplicate() +{ + _guide->duplicate(); + DocumentUndo::done(_guide->document, SP_VERB_NONE, _("Duplicate guide")); +} + +void GuidelinePropertiesDialog::_response(gint response) +{ + switch (response) { + case Gtk::RESPONSE_OK: + _onOK(); + break; + case -12: + _onDelete(); + break; + case -13: + _onDuplicate(); + break; + case Gtk::RESPONSE_CANCEL: + break; + case Gtk::RESPONSE_DELETE_EVENT: + break; + default: + g_assert_not_reached(); + } +} + +void GuidelinePropertiesDialog::_setup() { + set_title(_("Guideline")); + add_button(_("_OK"), Gtk::RESPONSE_OK); + add_button(_("_Duplicate"), -13); + add_button(_("_Delete"), -12); + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _layout_table.set_border_width(4); + + mainVBox->pack_start(_layout_table, false, false, 0); + + _label_name.set_label("foo0"); + _label_name.set_halign(Gtk::ALIGN_START); + _label_name.set_valign(Gtk::ALIGN_CENTER); + + _label_descr.set_label("foo1"); + _label_descr.set_halign(Gtk::ALIGN_START); + _label_descr.set_valign(Gtk::ALIGN_CENTER); + + _label_name.set_halign(Gtk::ALIGN_FILL); + _label_name.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_name, 0, 0, 3, 1); + + _label_descr.set_halign(Gtk::ALIGN_FILL); + _label_descr.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_descr, 0, 1, 3, 1); + + _label_entry.set_halign(Gtk::ALIGN_FILL); + _label_entry.set_valign(Gtk::ALIGN_FILL); + _label_entry.set_hexpand(); + _layout_table.attach(_label_entry, 1, 2, 2, 1); + + _color.set_halign(Gtk::ALIGN_FILL); + _color.set_valign(Gtk::ALIGN_FILL); + _color.set_hexpand(); + _color.set_margin_end(6); + _layout_table.attach(_color, 1, 3, 2, 1); + + // unitmenus + /* fixme: We should allow percents here too, as percents of the canvas size */ + _unit_menu.setUnitType(UNIT_TYPE_LINEAR); + _unit_menu.setUnit("px"); + if (_desktop->namedview->display_units) { + _unit_menu.setUnit( _desktop->namedview->display_units->abbr ); + } + _spin_angle.setUnit(_angle_unit_status); + + // position spinbuttons + _spin_button_x.setDigits(3); + _spin_button_x.setIncrements(1.0, 10.0); + _spin_button_x.setRange(-1e6, 1e6); + _spin_button_y.setDigits(3); + _spin_button_y.setIncrements(1.0, 10.0); + _spin_button_y.setRange(-1e6, 1e6); + + _spin_button_x.set_halign(Gtk::ALIGN_FILL); + _spin_button_x.set_valign(Gtk::ALIGN_FILL); + _spin_button_x.set_hexpand(); + _layout_table.attach(_spin_button_x, 1, 4, 1, 1); + + _spin_button_y.set_halign(Gtk::ALIGN_FILL); + _spin_button_y.set_valign(Gtk::ALIGN_FILL); + _spin_button_y.set_hexpand(); + _layout_table.attach(_spin_button_y, 1, 5, 1, 1); + + _unit_menu.set_halign(Gtk::ALIGN_FILL); + _unit_menu.set_valign(Gtk::ALIGN_FILL); + _unit_menu.set_margin_end(6); + _layout_table.attach(_unit_menu, 2, 4, 1, 1); + + // angle spinbutton + _spin_angle.setDigits(3); + _spin_angle.setIncrements(1.0, 10.0); + _spin_angle.setRange(-3600., 3600.); + + _spin_angle.set_halign(Gtk::ALIGN_FILL); + _spin_angle.set_valign(Gtk::ALIGN_FILL); + _spin_angle.set_hexpand(); + _layout_table.attach(_spin_angle, 1, 6, 2, 1); + + // mode radio button + _relative_toggle.set_halign(Gtk::ALIGN_FILL); + _relative_toggle.set_valign(Gtk::ALIGN_FILL); + _relative_toggle.set_hexpand(); + _relative_toggle.set_margin_start(6); + _layout_table.attach(_relative_toggle, 1, 7, 2, 1); + + // locked radio button + _locked_toggle.set_halign(Gtk::ALIGN_FILL); + _locked_toggle.set_valign(Gtk::ALIGN_FILL); + _locked_toggle.set_hexpand(); + _locked_toggle.set_margin_start(6); + _layout_table.attach(_locked_toggle, 1, 8, 2, 1); + + _relative_toggle.signal_toggled().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_modeChanged)); + _relative_toggle.set_active(_relative_toggle_status); + + bool global_guides_lock = _desktop->namedview->lockguides; + if(global_guides_lock){ + _locked_toggle.set_sensitive(false); + } + _locked_toggle.set_active(_guide->getLocked()); + + // don't know what this exactly does, but it results in that the dialog closes when entering a value and pressing enter (see LP bug 484187) + g_signal_connect_swapped(G_OBJECT(_spin_button_x.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_button_y.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_angle.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + + + // dialog + set_default_response(Gtk::RESPONSE_OK); + signal_response().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_response)); + + // initialize dialog + _oldpos = _guide->getPoint(); + if (_guide->isVertical()) { + _oldangle = 90; + } else if (_guide->isHorizontal()) { + _oldangle = 0; + } else { + _oldangle = Geom::deg_from_rad( std::atan2( - _guide->getNormal()[Geom::X], _guide->getNormal()[Geom::Y] ) ); + } + + { + // FIXME holy crap!!! + Inkscape::XML::Node *repr = _guide->getRepr(); + const gchar *guide_id = repr->attribute("id"); + gchar *label = g_strdup_printf(_("Guideline ID: %s"), guide_id); + _label_name.set_label(label); + g_free(label); + } + { + gchar *guide_description = _guide->description(false); + gchar *label = g_strdup_printf(_("Current: %s"), guide_description); + g_free(guide_description); + _label_descr.set_markup(label); + g_free(label); + } + + // init name entry + _label_entry.getEntry()->set_text(_guide->getLabel() ? _guide->getLabel() : ""); + + Gdk::RGBA c; + c.set_rgba(((_guide->getColor()>>24)&0xff) / 255.0, ((_guide->getColor()>>16)&0xff) / 255.0, ((_guide->getColor()>>8)&0xff) / 255.0); + _color.set_rgba(c); + + _modeChanged(); // sets values of spinboxes. + + if ( _oldangle == 90. || _oldangle == 270. || _oldangle == -90. || _oldangle == -270.) { + _spin_button_x.grabFocusAndSelectEntry(); + } else if ( _oldangle == 0. || _oldangle == 180. || _oldangle == -180.) { + _spin_button_y.grabFocusAndSelectEntry(); + } else { + _spin_angle.grabFocusAndSelectEntry(); + } + + set_position(Gtk::WIN_POS_MOUSE); + + show_all_children(); + set_modal(true); + _desktop->setWindowTransient (gobj()); + property_destroy_with_parent() = true; +} + +} +} +} + +/* + 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 : diff --git a/src/ui/dialog/guides.h b/src/ui/dialog/guides.h new file mode 100644 index 0000000..abfc7d3 --- /dev/null +++ b/src/ui/dialog/guides.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Andrius R. + * Johan Engelen + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_GUIDELINE_H +#define INKSCAPE_DIALOG_GUIDELINE_H + +#include +#include +#include +#include +#include + +#include "ui/widget/unit-menu.h" +#include "ui/widget/scalar-unit.h" +#include "ui/widget/entry.h" +#include <2geom/point.h> + +class SPGuide; +class SPDesktop; + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +}; + +namespace Dialogs { + +/** + * Dialog for modifying guidelines. + */ +class GuidelinePropertiesDialog : public Gtk::Dialog { +public: + GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop); + ~GuidelinePropertiesDialog() override; + + Glib::ustring getName() const { return "GuidelinePropertiesDialog"; } + + static void showDialog(SPGuide *guide, SPDesktop *desktop); + +protected: + void _setup(); + + void _onOK(); + void _onDelete(); + void _onDuplicate(); + + void _response(gint response); + void _modeChanged(); + +private: + GuidelinePropertiesDialog(GuidelinePropertiesDialog const &) = delete; // no copy + GuidelinePropertiesDialog &operator=(GuidelinePropertiesDialog const &) = delete; // no assign + + SPDesktop *_desktop; + SPGuide *_guide; + + Gtk::Grid _layout_table; + Gtk::Label _label_name; + Gtk::Label _label_descr; + Gtk::CheckButton _locked_toggle; + Gtk::CheckButton _relative_toggle; + static bool _relative_toggle_status; // remember the status of the _relative_toggle_status button across instances + Inkscape::UI::Widget::UnitMenu _unit_menu; + Inkscape::UI::Widget::ScalarUnit _spin_button_x; + Inkscape::UI::Widget::ScalarUnit _spin_button_y; + Inkscape::UI::Widget::Entry _label_entry; + Gtk::ColorButton _color; + + Inkscape::UI::Widget::ScalarUnit _spin_angle; + static Glib::ustring _angle_unit_status; // remember the status of the _relative_toggle_status button across instances + + bool _mode; + Geom::Point _oldpos; + gdouble _oldangle; +}; + +} // namespace +} // namespace +} // namespace + + +#endif // INKSCAPE_DIALOG_GUIDELINE_H + +/* + 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 : diff --git a/src/ui/dialog/icon-preview.cpp b/src/ui/dialog/icon-preview.cpp new file mode 100644 index 0000000..f8e73e3 --- /dev/null +++ b/src/ui/dialog/icon-preview.cpp @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2005,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include +#include +#include + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "icon-preview.h" + +#include "ui/widget/frame.h" + +extern "C" { +// takes doc, drawing, icon, and icon name to produce pixels +guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + const gchar *name, unsigned int psize, unsigned &stride); +} + +#define noICON_VERBOSE 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +IconPreviewPanel &IconPreviewPanel::getInstance() +{ + IconPreviewPanel *instance = new IconPreviewPanel(); + + instance->refreshPreview(); + + return *instance; +} + +//######################################################################### +//## E V E N T S +//######################################################################### + +void IconPreviewPanel::on_button_clicked(int which) +{ + if ( hot != which ) { + buttons[hot]->set_active( false ); + + hot = which; + updateMagnify(); + _getContents()->queue_draw(); + } +} + + + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +IconPreviewPanel::IconPreviewPanel() : + UI::Widget::Panel("/dialogs/iconpreview", SP_VERB_VIEW_ICON_PREVIEW), + deskTrack(), + desktop(nullptr), + document(nullptr), + drawing(nullptr), + visionkey(0), + timer(nullptr), + renderTimer(nullptr), + pending(false), + minDelay(0.1), + targetId(), + hot(1), + selectionButton(nullptr), + desktopChangeConn(), + docReplacedConn(), + docModConn(), + selChangedConn() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + numEntries = 0; + + bool pack = prefs->getBool("/iconpreview/pack", true); + + std::vector pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default"); + std::vector rawSizes; + + for (auto & pref_size : pref_sizes) { + if (prefs->getBool(pref_size + "/show", true)) { + int sizeVal = prefs->getInt(pref_size + "/value", -1); + if (sizeVal > 0) { + rawSizes.push_back(sizeVal); + } + } + } + + if ( !rawSizes.empty() ) { + numEntries = rawSizes.size(); + sizes = new int[numEntries]; + int i = 0; + for ( std::vector::iterator it = rawSizes.begin(); it != rawSizes.end(); ++it, ++i ) { + sizes[i] = *it; + } + } + + if ( numEntries < 1 ) + { + numEntries = 5; + sizes = new int[numEntries]; + sizes[0] = 16; + sizes[1] = 24; + sizes[2] = 32; + sizes[3] = 48; + sizes[4] = 128; + } + + pixMem = new guchar*[numEntries]; + images = new Gtk::Image*[numEntries]; + labels = new Glib::ustring*[numEntries]; + buttons = new Gtk::ToggleToolButton*[numEntries]; + + + for ( int i = 0; i < numEntries; i++ ) { + char *label = g_strdup_printf(_("%d x %d"), sizes[i], sizes[i]); + labels[i] = new Glib::ustring(label); + g_free(label); + pixMem[i] = nullptr; + images[i] = nullptr; + } + + + magLabel.set_label( *labels[hot] ); + + Gtk::VBox* magBox = new Gtk::VBox(); + + UI::Widget::Frame *magFrame = Gtk::manage(new UI::Widget::Frame(_("Magnified:"))); + magFrame->add( magnified ); + + magBox->pack_start( *magFrame, Gtk::PACK_EXPAND_WIDGET ); + magBox->pack_start( magLabel, Gtk::PACK_SHRINK ); + + + Gtk::VBox *verts = new Gtk::VBox(); + Gtk::HBox *horiz = nullptr; + int previous = 0; + int avail = 0; + for ( int i = numEntries - 1; i >= 0; --i ) { + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + pixMem[i] = new guchar[sizes[i] * stride]; + memset( pixMem[i], 0x00, sizes[i] * stride ); + + GdkPixbuf *pb = gdk_pixbuf_new_from_data( pixMem[i], GDK_COLORSPACE_RGB, TRUE, 8, sizes[i], sizes[i], stride, /*(GdkPixbufDestroyNotify)g_free*/nullptr, nullptr ); + GtkImage* img = GTK_IMAGE( gtk_image_new_from_pixbuf( pb ) ); + images[i] = Glib::wrap(img); + Glib::ustring label(*labels[i]); + buttons[i] = new Gtk::ToggleToolButton(label); + buttons[i]->set_active( i == hot ); + if ( prefs->getBool("/iconpreview/showFrames", true) ) { + Gtk::Frame *frame = new Gtk::Frame(); + frame->set_shadow_type(Gtk::SHADOW_ETCHED_IN); + frame->add(*images[i]); + buttons[i]->set_icon_widget(*Gtk::manage(frame)); + } else { + buttons[i]->set_icon_widget(*images[i]); + } + + buttons[i]->set_tooltip_text(label); + + buttons[i]->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &IconPreviewPanel::on_button_clicked), i) ); + + buttons[i]->set_halign(Gtk::ALIGN_CENTER); + buttons[i]->set_valign(Gtk::ALIGN_CENTER); + + if ( !pack || ( (avail == 0) && (previous == 0) ) ) { + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + previous = sizes[i]; + avail = sizes[i]; + } else { + int pad = 12; + if ((avail < pad) || ((sizes[i] > avail) && (sizes[i] < previous))) { + horiz = nullptr; + } + if ((horiz == nullptr) && (sizes[i] <= previous)) { + avail = previous; + } + if (sizes[i] <= avail) { + if (!horiz) { + horiz = Gtk::manage(new Gtk::HBox()); + avail = previous; + verts->pack_end(*horiz, Gtk::PACK_SHRINK); + } + horiz->pack_start(*(buttons[i]), Gtk::PACK_EXPAND_WIDGET); + avail -= sizes[i]; + avail -= pad; // a little extra for padding + } else { + horiz = nullptr; + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + } + } + } + + iconBox.pack_start(splitter); + splitter.pack1( *magBox, true, false ); + UI::Widget::Frame *actuals = Gtk::manage(new UI::Widget::Frame (_("Actual Size:"))); + actuals->set_border_width(4); + actuals->add(*verts); + splitter.pack2( *actuals, false, false ); + + + selectionButton = new Gtk::CheckButton(C_("Icon preview window", "Sele_ction"), true);//selectionButton = (Gtk::ToggleButton*) gtk_check_button_new_with_mnemonic(_("_Selection")); // , GTK_RESPONSE_APPLY + magBox->pack_start( *selectionButton, Gtk::PACK_SHRINK ); + selectionButton->set_tooltip_text(_("Selection only or whole document")); + selectionButton->signal_clicked().connect( sigc::mem_fun(*this, &IconPreviewPanel::modeToggled) ); + + gint val = prefs->getBool("/iconpreview/selectionOnly"); + selectionButton->set_active( val != 0 ); + + + _getContents()->pack_start(iconBox, Gtk::PACK_SHRINK); + + show_all_children(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &IconPreviewPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +IconPreviewPanel::~IconPreviewPanel() +{ + setDesktop(nullptr); + if (timer) { + timer->stop(); + delete timer; + timer = nullptr; + } + if ( renderTimer ) { + renderTimer->stop(); + delete renderTimer; + renderTimer = nullptr; + } + + selChangedConn.disconnect(); + docModConn.disconnect(); + docReplacedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +//######################################################################### +//## M E T H O D S +//######################################################################### + + +#if ICON_VERBOSE +static Glib::ustring getTimestr() +{ + Glib::ustring str; + gint64 micr = g_get_monotonic_time(); + gint64 mins = ((int)round(micr / 60000000)) % 60; + gdouble dsecs = micr / 1000000; + gchar *ptr = g_strdup_printf(":%02u:%f", mins, dsecs); + str = ptr; + g_free(ptr); + ptr = 0; + return str; +} +#endif // ICON_VERBOSE + +void IconPreviewPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + SPDocument *newDoc = (desktop) ? desktop->doc() : nullptr; + + if ( desktop != this->desktop ) { + docReplacedConn.disconnect(); + selChangedConn.disconnect(); + + this->desktop = Panel::getDesktop(); + if ( this->desktop ) { + docReplacedConn = this->desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(this, &IconPreviewPanel::setDocument))); + if ( this->desktop->selection && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + selChangedConn = this->desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + } + } + setDocument(newDoc); + deskTrack.setBase(desktop); +} + +void IconPreviewPanel::setDocument( SPDocument *document ) +{ + if (this->document != document) { + docModConn.disconnect(); + if (drawing) { + this->document->getRoot()->invoke_hide(visionkey); + delete drawing; + drawing = nullptr; + } + this->document = document; + if (this->document) { + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + drawing->setRoot(this->document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + + if ( Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + docModConn = this->document->connectModified(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + queueRefresh(); + } + } +} + +void IconPreviewPanel::refreshPreview() +{ + SPDesktop *desktop = getDesktop(); + if (!timer) { + timer = new Glib::Timer(); + } + if (timer->elapsed() < minDelay) { +#if ICON_VERBOSE + g_message( "%s Deferring refresh as too soon. calling queueRefresh()", getTimestr().c_str() ); +#endif //ICON_VERBOSE + // Do not refresh too quickly + queueRefresh(); + } else if ( desktop && desktop->doc() ) { +#if ICON_VERBOSE + g_message( "%s Refreshing preview.", getTimestr().c_str() ); +#endif // ICON_VERBOSE + bool hold = Inkscape::Preferences::get()->getBool("/iconpreview/selectionHold", true); + SPObject *target = nullptr; + if ( selectionButton && selectionButton->get_active() ) + { + target = (hold && !targetId.empty()) ? desktop->doc()->getObjectById( targetId.c_str() ) : nullptr; + if ( !target ) { + targetId.clear(); + Inkscape::Selection * sel = desktop->getSelection(); + if ( sel ) { + //g_message("found a selection to play with"); + + auto items = sel->items(); + for(auto i=items.begin();!target && i!=items.end();++i){ + SPItem* item = *i; + gchar const *id = item->getId(); + if ( id ) { + targetId = id; + target = item; + } + } + } + } + } else { + target = desktop->currentRoot(); + } + if ( target ) { + renderPreview(target); + } +#if ICON_VERBOSE + g_message( "%s resetting timer", getTimestr().c_str() ); +#endif // ICON_VERBOSE + timer->reset(); + } +} + +bool IconPreviewPanel::refreshCB() +{ + bool callAgain = true; + if (!timer) { + timer = new Glib::Timer(); + } + if ( timer->elapsed() > minDelay ) { +#if ICON_VERBOSE + g_message( "%s refreshCB() timer has progressed", getTimestr().c_str() ); +#endif // ICON_VERBOSE + callAgain = false; + refreshPreview(); +#if ICON_VERBOSE + g_message( "%s refreshCB() setting pending false", getTimestr().c_str() ); +#endif // ICON_VERBOSE + pending = false; + } + return callAgain; +} + +void IconPreviewPanel::queueRefresh() +{ + if (!pending) { + pending = true; +#if ICON_VERBOSE + g_message( "%s queueRefresh() Setting pending true", getTimestr().c_str() ); +#endif // ICON_VERBOSE + if (!timer) { + timer = new Glib::Timer(); + } + Glib::signal_idle().connect( sigc::mem_fun(this, &IconPreviewPanel::refreshCB), Glib::PRIORITY_DEFAULT_IDLE ); + } +} + +void IconPreviewPanel::modeToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool selectionOnly = (selectionButton && selectionButton->get_active()); + prefs->setBool("/iconpreview/selectionOnly", selectionOnly); + if ( !selectionOnly ) { + targetId.clear(); + } + + refreshPreview(); +} + +void overlayPixels(guchar *px, int width, int height, int stride, + unsigned r, unsigned g, unsigned b) +{ + int bytesPerPixel = 4; + int spacing = 4; + for ( int y = 0; y < height; y += spacing ) { + guchar *ptr = px + y * stride; + for ( int x = 0; x < width; x += spacing ) { + *(ptr++) = r; + *(ptr++) = g; + *(ptr++) = b; + *(ptr++) = 0xff; + + ptr += bytesPerPixel * (spacing - 1); + } + } + + if ( width > 1 && height > 1 ) { + // point at the last pixel + guchar *ptr = px + ((height-1) * stride) + ((width - 1) * bytesPerPixel); + + if ( width > 2 ) { + px[4] = r; + px[5] = g; + px[6] = b; + px[7] = 0xff; + + ptr[-12] = r; + ptr[-11] = g; + ptr[-10] = b; + ptr[-9] = 0xff; + } + + ptr[-4] = r; + ptr[-3] = g; + ptr[-2] = b; + ptr[-1] = 0xff; + + px[0 + stride] = r; + px[1 + stride] = g; + px[2 + stride] = b; + px[3 + stride] = 0xff; + + ptr[0 - stride] = r; + ptr[1 - stride] = g; + ptr[2 - stride] = b; + ptr[3 - stride] = 0xff; + + if ( height > 2 ) { + ptr[0 - stride * 3] = r; + ptr[1 - stride * 3] = g; + ptr[2 - stride * 3] = b; + ptr[3 - stride * 3] = 0xff; + } + } +} + +// takes doc, drawing, icon, and icon name to produce pixels +extern "C" guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + gchar const *name, unsigned psize, + unsigned &stride) +{ + bool const dump = Inkscape::Preferences::get()->getBool("/debug/icons/dumpSvg"); + guchar *px = nullptr; + + if (doc) { + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_ITEM(object)) { + SPItem *item = SP_ITEM(object); + // Find bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if ( object->parent == nullptr ) + { + dbox = Geom::Rect(Geom::Point(0, 0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + } + + /* This is in document coordinates, i.e. pixels */ + if ( dbox ) { + /* Update to renderable state */ + double sf = 1.0; + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + /* Item integer bbox in points */ + // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5) + Geom::IntRect ibox = dbox->roundOutwards(); + + if ( dump ) { + g_message( " box --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + int width = ibox.width(); + int height = ibox.height(); + + if ( dump ) { + g_message( " vis --'%s' (%d,%d)", name, width, height ); + } + + { + int block = std::max(width, height); + if (block != static_cast(psize) ) { + if ( dump ) { + g_message(" resizing" ); + } + sf = (double)psize / (double)block; + + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + + auto scaled_box = *dbox * Geom::Scale(sf); + ibox = scaled_box.roundOutwards(); + if ( dump ) { + g_message( " box2 --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + width = ibox.width(); + height = ibox.height(); + if ( dump ) { + g_message( " vis2 --'%s' (%d,%d)", name, width, height ); + } + } + } + + Geom::IntPoint pdim(psize, psize); + int dx, dy; + //dx = (psize - width) / 2; + //dy = (psize - height) / 2; + dx=dy=psize; + dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative + dy=(dy-height)/2; + Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim); + /* Actual renderable area */ + Geom::IntRect ua = *Geom::intersect(ibox, area); + + if ( dump ) { + g_message( " area --'%s' (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() ); + g_message( " ua --'%s' (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() ); + } + + stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize); + + /* Set up pixblock */ + px = g_new(guchar, stride * psize); + memset(px, 0x00, stride * psize); + + /* Render */ + cairo_surface_t *s = cairo_image_surface_create_for_data(px, + CAIRO_FORMAT_ARGB32, psize, psize, stride); + Inkscape::DrawingContext dc(s, ua.min()); + + SPNamedView *nv = sp_document_namedview(doc, nullptr); + float bg_r = SP_RGBA32_R_F(nv->pagecolor); + float bg_g = SP_RGBA32_G_F(nv->pagecolor); + float bg_b = SP_RGBA32_B_F(nv->pagecolor); + float bg_a = SP_RGBA32_A_F(nv->pagecolor); + + cairo_t *cr = cairo_create(s); + cairo_set_source_rgba(cr, bg_r, bg_g, bg_b, bg_a); + cairo_rectangle(cr, 0, 0, psize, psize); + cairo_fill(cr); + cairo_save(cr); + cairo_destroy(cr); + + drawing.render(dc, ua); + cairo_surface_destroy(s); + + // convert to GdkPixbuf format + convert_pixels_argb32_to_pixbuf(px, psize, psize, stride); + + if ( Inkscape::Preferences::get()->getBool("/debug/icons/overlaySvg") ) { + overlayPixels( px, psize, psize, stride, 0x00, 0x00, 0xff ); + } + } + } + } + + return px; +} // end of sp_icon_doc_icon() + + +void IconPreviewPanel::renderPreview( SPObject* obj ) +{ + SPDocument * doc = obj->document; + gchar const * id = obj->getId(); + if ( !renderTimer ) { + renderTimer = new Glib::Timer(); + } + renderTimer->reset(); + +#if ICON_VERBOSE + g_message("%s setting up to render '%s' as the icon", getTimestr().c_str(), id ); +#endif // ICON_VERBOSE + + for ( int i = 0; i < numEntries; i++ ) { + unsigned unused; + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + guchar *px = sp_icon_doc_icon(doc, *drawing, id, sizes[i], unused); +// g_message( " size %d %s", sizes[i], (px ? "worked" : "failed") ); + if ( px ) { + memcpy( pixMem[i], px, sizes[i] * stride ); + g_free( px ); + px = nullptr; + } else { + memset( pixMem[i], 0, sizes[i] * stride ); + } + images[i]->set(images[i]->get_pixbuf()); + // images[i]->queue_draw(); + } + updateMagnify(); + + renderTimer->stop(); + minDelay = std::max( 0.1, renderTimer->elapsed() * 3.0 ); +#if ICON_VERBOSE + g_message(" render took %f seconds.", renderTimer->elapsed()); +#endif // ICON_VERBOSE +} + +void IconPreviewPanel::updateMagnify() +{ + Glib::RefPtr buf = images[hot]->get_pixbuf()->scale_simple( 128, 128, Gdk::INTERP_NEAREST ); + magLabel.set_label( *labels[hot] ); + magnified.set( buf ); + // magnified.queue_draw(); + // magnified.get_parent()->queue_draw(); +} + +} //namespace Dialogs +} //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 : diff --git a/src/ui/dialog/icon-preview.h b/src/ui/dialog/icon-preview.h new file mode 100644 index 0000000..938bbbf --- /dev/null +++ b/src/ui/dialog/icon-preview.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004,2005 The Inkscape Organization + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_ICON_PREVIEW_H +#define SEEN_ICON_PREVIEW_H + +#include +#include +#include +#include +#include +#include +#include + +#include "ui/widget/panel.h" +#include "desktop-tracker.h" + +class SPObject; +namespace Glib { +class Timer; +} + +namespace Inkscape { +class Drawing; +namespace UI { +namespace Dialog { + + +/** + * A panel that displays an icon preview + */ +class IconPreviewPanel : public UI::Widget::Panel +{ +public: + IconPreviewPanel(); + //IconPreviewPanel(Glib::ustring const &label); + ~IconPreviewPanel() override; + + static IconPreviewPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void refreshPreview(); + void modeToggled(); + +private: + IconPreviewPanel(IconPreviewPanel const &) = delete; // no copy + IconPreviewPanel &operator=(IconPreviewPanel const &) = delete; // no assign + + + DesktopTracker deskTrack; + SPDesktop *desktop; + SPDocument *document; + Drawing *drawing; + unsigned int visionkey; + Glib::Timer *timer; + Glib::Timer *renderTimer; + bool pending; + gdouble minDelay; + + Gtk::VBox iconBox; + Gtk::Paned splitter; + Glib::ustring targetId; + int hot; + int numEntries; + int* sizes; + + Gtk::Image magnified; + Gtk::Label magLabel; + + Gtk::ToggleButton *selectionButton; + + guchar** pixMem; + Gtk::Image** images; + Glib::ustring** labels; + Gtk::ToggleToolButton** buttons; + sigc::connection desktopChangeConn; + sigc::connection docReplacedConn; + sigc::connection docModConn; + sigc::connection selChangedConn; + + + void setDocument( SPDocument *document ); + void on_button_clicked(int which); + void renderPreview( SPObject* obj ); + void updateMagnify(); + void queueRefresh(); + bool refreshCB(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_ICON_PREVIEW_H + +/* + 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 : diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp new file mode 100644 index 0000000..72a8107 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -0,0 +1,2772 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape Preferences dialog - implementation. + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen + * Bruno Dilly + * + * Copyright (C) 2004-2013 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "inkscape-preferences.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "auto-save.h" +#include "cms-system.h" +#include "document.h" +#include "enums.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "message-stack.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selcue.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "shortcuts.h" +#include "verbs.h" + +/* #include "display/cairo-utils.h" */ +#include "display/canvas-grid.h" +#include "display/nr-filter-gaussian.h" + +#include "extension/internal/gdkpixbuf-input.h" + +#include "include/gtkmm_version.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/color-profile.h" +#include "style.h" +#include "svg/svg-color.h" +#include "ui/interface.h" +#include "ui/widget/style-swatch.h" +#include "widgets/desktop-widget.h" +#include + +#if HAVE_ASPELL +# include "ui/dialog/spellcheck.h" // for get_available_langs +# ifdef _WIN32 +# include +# endif +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::DialogPage; +using Inkscape::UI::Widget::PrefCheckButton; +using Inkscape::UI::Widget::PrefRadioButton; +using Inkscape::UI::Widget::PrefSpinButton; +using Inkscape::UI::Widget::StyleSwatch; +using Inkscape::CMSSystem; + +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + x.erase(x.find_last_not_of(' ') + 1); + +InkscapePreferences::InkscapePreferences() + : UI::Widget::Panel ("/dialogs/preferences", SP_VERB_DIALOG_DISPLAY), + _minimum_width(0), + _minimum_height(0), + _natural_width(0), + _natural_height(0), + _current_page(nullptr), + _init(true) +{ + //get the width of a spinbutton + Inkscape::UI::Widget::SpinButton* sb = new Inkscape::UI::Widget::SpinButton; + sb->set_width_chars(6); + _getContents()->add(*sb); + show_all_children(); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + sb->get_preferred_size(sreq_natural, sreq); + _sb_width = sreq.width; + _getContents()->remove(*sb); + delete sb; + + //Main HBox + auto hbox_list_page = Gtk::manage(new Gtk::Box()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + _getContents()->add(*hbox_list_page); + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->set_propagate_natural_width(); + scrolled_window->set_propagate_natural_height(); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _page_list.append_column("name",_page_list_columns._col_name); + Glib::RefPtr page_list_selection = _page_list.get_selection(); + page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + //Pages + auto vbox_page = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Frame* title_frame = Gtk::manage(new Gtk::Frame()); + + Gtk::ScrolledWindow* pageScroller = Gtk::manage(new Gtk::ScrolledWindow()); + pageScroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + pageScroller->set_propagate_natural_width(); + pageScroller->set_propagate_natural_height(); + pageScroller->add(*vbox_page); + hbox_list_page->pack_start(*pageScroller, true, true, 0); + + title_frame->add(_page_title); + vbox_page->pack_start(*title_frame, false, false, 0); + vbox_page->pack_start(_page_frame, true, true, 0); + _page_frame.set_shadow_type(Gtk::SHADOW_IN); + title_frame->set_shadow_type(Gtk::SHADOW_IN); + + initPageTools(); + initPageUI(); + initPageBehavior(); + initPageIO(); + + initPageSystem(); + initPageBitmaps(); + initPageRendering(); + initPageSpellcheck(); + + + signalPresent().connect(sigc::mem_fun(*this, &InkscapePreferences::_presentPages)); + + //calculate the size request for this dialog + _page_list.expand_all(); + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::GetSizeRequest)); + _page_list.collapse_all(); +} + +InkscapePreferences::~InkscapePreferences() += default; + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, int id) +{ + return AddPage(p, title, Gtk::TreeModel::iterator() , id); +} + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id) +{ + Gtk::TreeModel::iterator iter; + if (parent) + iter = _page_list_model->append((*parent).children()); + else + iter = _page_list_model->append(); + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = title; + row[_page_list_columns._col_id] = id; + row[_page_list_columns._col_page] = &p; + return iter; +} + +void InkscapePreferences::AddSelcueCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show selection cue"), prefs_path + "/selcue", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display a selection cue (the same as in selector)")); +} + +void InkscapePreferences::AddGradientCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Enable gradient editing"), prefs_path + "/gradientdrag", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display gradient editing controls")); +} + +void InkscapePreferences::AddConvertGuidesCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Conversion to guides uses edges instead of bounding box"), prefs_path + "/convertguides", def_value); + p.add_line( false, "", *cb, "", _("Converting an object to guides places these along the object's true edges (imitating the object's shape), not along the bounding box")); +} + +void InkscapePreferences::AddDotSizeSpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/dot-size", 0.0, 1000.0, 0.1, 10.0, def_value, false, false); + p.add_line( false, _("Ctrl+click _dot size:"), *sb, _("times current stroke width"), + _("Size of dots created with Ctrl+click (relative to current stroke width)"), + false ); +} + +void InkscapePreferences::AddBaseSimplifySpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/base-simplify", 0.0, 100.0, 1.0, 10.0, def_value, false, false); + p.add_line( false, _("Base simplify:"), *sb, _("on dynamic LPE simplify"), + _("Base simplify of dynamic LPE based simplify"), + false ); +} + + +static void StyleFromSelectionToTool(Glib::ustring const &prefs_path, StyleSwatch *swatch) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) + return; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("No objects selected to take the style from.")); + return; + } + SPItem *item = selection->singleItem(); + if (!item) { + /* TODO: If each item in the selection has the same style then don't consider it an error. + * Maybe we should try to handle multiple selections anyway, e.g. the intersection of the + * style attributes for the selected items. */ + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("More than one object selected. Cannot take style from multiple objects.")); + return; + } + + SPCSSAttr *css = take_style_from_item (item); + + if (!css) return; + + // remove black-listed properties + css = sp_css_attr_unset_blacklist (css); + + // only store text style for the text tool + if (prefs_path != "/tools/text") { + css = sp_css_attr_unset_text (css); + } + + // we cannot store properties with uris - they will be invalid in other documents + css = sp_css_attr_unset_uris (css); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setStyle(prefs_path + "/style", css); + sp_repr_css_attr_unref (css); + + // update the swatch + if (swatch) { + SPCSSAttr *css = prefs->getInheritedStyle(prefs_path + "/style"); + swatch->setStyle (css); + sp_repr_css_attr_unref(css); + } +} + +void InkscapePreferences::AddNewObjectsStyle(DialogPage &p, Glib::ustring const &prefs_path, const gchar *banner) +{ + if (banner) + p.add_group_header(banner); + else + p.add_group_header( _("Style of new objects")); + PrefRadioButton* current = Gtk::manage( new PrefRadioButton); + current->init ( _("Last used style"), prefs_path + "/usecurrent", 1, true, nullptr); + p.add_line( true, "", *current, "", + _("Apply the style you last set on an object")); + + PrefRadioButton* own = Gtk::manage( new PrefRadioButton); + auto hb = Gtk::manage( new Gtk::Box); + own->init ( _("This tool's own style:"), prefs_path + "/usecurrent", 0, false, current); + own->set_halign(Gtk::ALIGN_START); + own->set_valign(Gtk::ALIGN_START); + hb->add(*own); + p.set_tip( *own, _("Each tool may store its own style to apply to the newly created objects. Use the button below to set it.")); + p.add_line( true, "", *hb, "", ""); + + // style swatch + Gtk::Button* button = Gtk::manage( new Gtk::Button(_("Take from selection"), true)); + StyleSwatch *swatch = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getInt(prefs_path + "/usecurrent")) { + button->set_sensitive(false); + } + + SPCSSAttr *css = prefs->getStyle(prefs_path + "/style"); + swatch = new StyleSwatch(css, _("This tool's style of new objects")); + hb->add(*swatch); + sp_repr_css_attr_unref(css); + + button->signal_clicked().connect( sigc::bind( sigc::ptr_fun(StyleFromSelectionToTool), prefs_path, swatch) ); + own->changed_signal.connect( sigc::mem_fun(*button, &Gtk::Button::set_sensitive) ); + p.add_line( true, "", *button, "", + _("Remember the style of the (first) selected object as this tool's style")); +} + +void InkscapePreferences::initPageTools() +{ + Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS); + this->AddPage(_page_selector, _("Selector"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR); + this->AddPage(_page_node, _("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE); + + // shapes + Gtk::TreeModel::iterator iter_shapes = this->AddPage(_page_shapes, _("Shapes"), iter_tools, PREFS_PAGE_TOOLS_SHAPES); + this->AddPage(_page_rectangle, _("Rectangle"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT); + this->AddPage(_page_ellipse, _("Ellipse"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE); + this->AddPage(_page_star, _("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR); + this->AddPage(_page_3dbox, _("3D Box"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX); + this->AddPage(_page_spiral, _("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL); + + this->AddPage(_page_pen, _("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN); + this->AddPage(_page_pencil, _("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL); + this->AddPage(_page_calligraphy, _("Calligraphy"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY); + this->AddPage(_page_text, C_("ContextVerb", "Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT); + + this->AddPage(_page_gradient, _("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT); + this->AddPage(_page_dropper, _("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER); + this->AddPage(_page_paintbucket, _("Paint Bucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET); + + this->AddPage(_page_tweak, _("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK); + this->AddPage(_page_spray, _("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY); + this->AddPage(_page_eraser, _("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER); + this->AddPage(_page_connector, _("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR); +#ifdef WITH_LPETOOL + this->AddPage(_page_lpetool, _("LPE Tool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL); +#endif // WITH_LPETOOL + this->AddPage(_page_zoom, _("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM); + this->AddPage(_page_measure, C_("ContextVerb", "Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE); + + _path_tools = _page_list.get_model()->get_path(iter_tools); + + _page_tools.add_group_header( _("Bounding box to use")); + _t_bbox_visual.init ( _("Visual bounding box"), "/tools/bounding_box", 0, false, nullptr); // 0 means visual + _page_tools.add_line( true, "", _t_bbox_visual, "", + _("This bounding box includes stroke width, markers, filter margins, etc.")); + _t_bbox_geometric.init ( _("Geometric bounding box"), "/tools/bounding_box", 1, true, &_t_bbox_visual); // 1 means geometric + _page_tools.add_line( true, "", _t_bbox_geometric, "", + _("This bounding box includes only the bare path")); + + _page_tools.add_group_header( _("Conversion to guides")); + _t_cvg_keep_objects.init ( _("Keep objects after conversion to guides"), "/tools/cvg_keep_objects", false); + _page_tools.add_line( true, "", _t_cvg_keep_objects, "", + _("When converting an object to guides, don't delete the object after the conversion")); + _t_cvg_convert_whole_groups.init ( _("Treat groups as a single object"), "/tools/cvg_convert_whole_groups", false); + _page_tools.add_line( true, "", _t_cvg_convert_whole_groups, "", + _("Treat groups as a single object during conversion to guides rather than converting each child separately")); + + _pencil_average_all_sketches.init ( _("Average all sketches"), "/tools/freehand/pencil/average_all_sketches", false); + _calligrapy_use_abs_size.init ( _("Width is in absolute units"), "/tools/calligraphic/abs_width", false); + _calligrapy_keep_selected.init ( _("Select new path"), "/tools/calligraphic/keep_selected", true); + _connector_ignore_text.init( _("Don't attach connectors to text objects"), "/tools/connector/ignoretext", true); + + //Selector + + + AddSelcueCheckbox(_page_selector, "/tools/select", false); + AddGradientCheckbox(_page_selector, "/tools/select", false); + _page_selector.add_group_header( _("When transforming, show")); + _t_sel_trans_obj.init ( _("Objects"), "/tools/select/show", "content", true, nullptr); + _page_selector.add_line( true, "", _t_sel_trans_obj, "", + _("Show the actual objects when moving or transforming")); + _t_sel_trans_outl.init ( _("Box outline"), "/tools/select/show", "outline", false, &_t_sel_trans_obj); + _page_selector.add_line( true, "", _t_sel_trans_outl, "", + _("Show only a box outline of the objects when moving or transforming")); + _page_selector.add_group_header( _("Per-object selection cue")); + _t_sel_cue_none.init ( C_("Selection cue", "None"), "/options/selcue/value", Inkscape::SelCue::NONE, false, nullptr); + _page_selector.add_line( true, "", _t_sel_cue_none, "", + _("No per-object selection indication")); + _t_sel_cue_mark.init ( _("Mark"), "/options/selcue/value", Inkscape::SelCue::MARK, true, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_mark, "", + _("Each selected object has a diamond mark in the top left corner")); + _t_sel_cue_box.init ( _("Box"), "/options/selcue/value", Inkscape::SelCue::BBOX, false, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_box, "", + _("Each selected object displays its bounding box")); + + //Node + AddSelcueCheckbox(_page_node, "/tools/nodes", true); + AddGradientCheckbox(_page_node, "/tools/nodes", true); + _page_node.add_group_header( _("Path outline")); + _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff); + _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline"), false); + _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false); + _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths")); + _t_node_live_outline.init(_("Update outline when dragging nodes"), "/tools/nodes/live_outline", false); + _page_node.add_line( true, "", _t_node_live_outline, "", _("Update the outline when dragging or transforming nodes; if this is off, the outline will only update when completing a drag")); + _t_node_live_objects.init(_("Update paths when dragging nodes"), "/tools/nodes/live_objects", false); + _page_node.add_line( true, "", _t_node_live_objects, "", _("Update paths when dragging or transforming nodes; if this is off, paths will only be updated when completing a drag")); + _t_node_show_path_direction.init(_("Show path direction on outlines"), "/tools/nodes/show_path_direction", false); + _page_node.add_line( true, "", _t_node_show_path_direction, "", _("Visualize the direction of selected paths by drawing small arrows in the middle of each outline segment")); + _t_node_pathflash_enabled.init ( _("Show temporary path outline"), "/tools/nodes/pathflash_enabled", false); + _page_node.add_line( true, "", _t_node_pathflash_enabled, "", _("When hovering over a path, briefly flash its outline")); + _t_node_pathflash_selected.init ( _("Show temporary outline for selected paths"), "/tools/nodes/pathflash_selected", false); + _page_node.add_line( true, "", _t_node_pathflash_selected, "", _("Show temporary outline even when a path is selected for editing")); + _t_node_pathflash_timeout.init("/tools/nodes/pathflash_timeout", 0, 10000.0, 100.0, 100.0, 1000.0, true, false); + _page_node.add_line( false, _("_Flash time:"), _t_node_pathflash_timeout, "ms", _("Specifies how long the path outline will be visible after a mouse-over (in milliseconds); specify 0 to have the outline shown until mouse leaves the path"), false); + _page_node.add_group_header(_("Editing preferences")); + _t_node_single_node_transform_handles.init(_("Show transform handles for single nodes"), "/tools/nodes/single_node_transform_handles", false); + _page_node.add_line( true, "", _t_node_single_node_transform_handles, "", _("Show transform handles even when only a single node is selected")); + _t_node_delete_preserves_shape.init(_("Deleting nodes preserves shape"), "/tools/nodes/delete_preserves_shape", true); + _page_node.add_line( true, "", _t_node_delete_preserves_shape, "", _("Move handles next to deleted nodes to resemble original shape; hold Ctrl to get the other behavior")); + + //Tweak + this->AddNewObjectsStyle(_page_tweak, "/tools/tweak", _("Object paint style")); + AddSelcueCheckbox(_page_tweak, "/tools/tweak", true); + AddGradientCheckbox(_page_tweak, "/tools/tweak", false); + + //Zoom + AddSelcueCheckbox(_page_zoom, "/tools/zoom", true); + AddGradientCheckbox(_page_zoom, "/tools/zoom", false); + + //Measure + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Ignore first and last points"), "/tools/measure/ignore_1st_and_last", true); + _page_measure.add_line( false, "", *cb, "", _("The start and end of the measurement tool's control line will not be considered for calculating lengths. Only lengths between actual curve intersections will be displayed.")); + + //Shapes + _path_shapes = _page_list.get_model()->get_path(iter_shapes); + this->AddSelcueCheckbox(_page_shapes, "/tools/shapes", true); + this->AddGradientCheckbox(_page_shapes, "/tools/shapes", true); + + //Rectangle + this->AddNewObjectsStyle(_page_rectangle, "/tools/shapes/rect"); + this->AddConvertGuidesCheckbox(_page_rectangle, "/tools/shapes/rect", true); + + //3D box + this->AddNewObjectsStyle(_page_3dbox, "/tools/shapes/3dbox"); + this->AddConvertGuidesCheckbox(_page_3dbox, "/tools/shapes/3dbox", true); + + //Ellipse + this->AddNewObjectsStyle(_page_ellipse, "/tools/shapes/arc"); + + //Star + this->AddNewObjectsStyle(_page_star, "/tools/shapes/star"); + + //Spiral + this->AddNewObjectsStyle(_page_spiral, "/tools/shapes/spiral"); + + //Pencil + this->AddSelcueCheckbox(_page_pencil, "/tools/freehand/pencil", true); + this->AddNewObjectsStyle(_page_pencil, "/tools/freehand/pencil"); + this->AddDotSizeSpinbutton(_page_pencil, "/tools/freehand/pencil", 3.0); + this->AddBaseSimplifySpinbutton(_page_pencil, "/tools/freehand/pencil", 25.0); + _page_pencil.add_group_header( _("Sketch mode")); + _page_pencil.add_line( true, "", _pencil_average_all_sketches, "", + _("If on, the sketch result will be the normal average of all sketches made, instead of averaging the old result with the new sketch")); + + //Pen + this->AddSelcueCheckbox(_page_pen, "/tools/freehand/pen", true); + this->AddNewObjectsStyle(_page_pen, "/tools/freehand/pen"); + this->AddDotSizeSpinbutton(_page_pen, "/tools/freehand/pen", 3.0); + + //Calligraphy + this->AddSelcueCheckbox(_page_calligraphy, "/tools/calligraphic", false); + this->AddNewObjectsStyle(_page_calligraphy, "/tools/calligraphic"); + _page_calligraphy.add_line( false, "", _calligrapy_use_abs_size, "", + _("If on, pen width is in absolute units (px) independent of zoom; otherwise pen width depends on zoom so that it looks the same at any zoom")); + _page_calligraphy.add_line( false, "", _calligrapy_keep_selected, "", + _("If on, each newly created object will be selected (deselecting previous selection)")); + + //Text + this->AddSelcueCheckbox(_page_text, "/tools/text", true); + this->AddGradientCheckbox(_page_text, "/tools/text", true); + { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show font samples in the drop-down list"), "/tools/text/show_sample_in_list", true); + _page_text.add_line( false, "", *cb, "", _("Show font samples alongside font names in the drop-down list in Text bar")); + + _font_dialog.init(_("Show font substitution warning dialog"), "/options/font/substitutedlg", false); + _page_text.add_line( false, "", _font_dialog, "", _("Show font substitution warning dialog when requested fonts are not available on the system")); + + cb = Gtk::manage(new PrefCheckButton); + cb->init ( _("Use SVG2 auto-flowed text"), "/tools/text/use_svg2", true); + _page_text.add_line( false, "", *cb, "", _("Use SVG2 auto-flowed text instead of SVG1.2 auto-flowed text. (Recommended)")); + } + + //_page_text.add_group_header( _("Text units")); + //_font_output_px.init ( _("Always output text size in pixels (px)"), "/options/font/textOutputPx", true); + //_page_text.add_line( true, "", _font_output_px, "", _("Always convert the text size units above into pixels (px) before saving to file")); + + _page_text.add_group_header( _("Font directories")); + _font_fontsdir_system.init( _("Use Inkscape's fonts directory"), "/options/font/use_fontsdir_system", true); + _page_text.add_line( true, "", _font_fontsdir_system, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's global \"share\" directory")); + _font_fontsdir_user.init( _("Use user's fonts directory"), "/options/font/use_fontsdir_user", true); + _page_text.add_line( true, "", _font_fontsdir_user, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's user configuration directory")); + _font_fontdirs_custom.init("/options/font/custom_fontdirs", 50); + _page_text.add_line(true, _("Additional font directories"), _font_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true); + + + this->AddNewObjectsStyle(_page_text, "/tools/text"); + + //Spray + AddSelcueCheckbox(_page_spray, "/tools/spray", true); + AddGradientCheckbox(_page_spray, "/tools/spray", false); + + //Eraser + this->AddNewObjectsStyle(_page_eraser, "/tools/eraser"); + + //Paint Bucket + this->AddSelcueCheckbox(_page_paintbucket, "/tools/paintbucket", false); + this->AddNewObjectsStyle(_page_paintbucket, "/tools/paintbucket"); + + //Gradient + this->AddSelcueCheckbox(_page_gradient, "/tools/gradient", true); + _misc_forkvectors.init( _("Prevent sharing of gradient definitions"), "/options/forkgradientvectors/value", true); + _page_gradient.add_line( false, "", _misc_forkvectors, "", + _("When on, shared gradient definitions are automatically forked on change; uncheck to allow sharing of gradient definitions so that editing one object may affect other objects using the same gradient"), true); + _misc_gradienteditor.init( _("Use legacy Gradient Editor"), "/dialogs/gradienteditor/showlegacy", false); + _page_gradient.add_line( false, "", _misc_gradienteditor, "", + _("When on, the Gradient Edit button in the Fill & Stroke dialog will show the legacy Gradient Editor dialog, when off the Gradient Tool will be used"), true); + + _misc_gradientangle.init("/dialogs/gradienteditor/angle", -359, 359, 1, 90, 0, false, false); + _page_gradient.add_line( false, _("Linear gradient _angle:"), _misc_gradientangle, "", + _("Default angle of new linear gradients in degrees (clockwise from horizontal)"), false); + + + //Dropper + this->AddSelcueCheckbox(_page_dropper, "/tools/dropper", true); + this->AddGradientCheckbox(_page_dropper, "/tools/dropper", true); + + //Connector + this->AddSelcueCheckbox(_page_connector, "/tools/connector", true); + _page_connector.add_line(false, "", _connector_ignore_text, "", + _("If on, connector attachment points will not be shown for text objects")); + +#ifdef WITH_LPETOOL + //LPETool + //disabled, because the LPETool is not finished yet. + this->AddNewObjectsStyle(_page_lpetool, "/tools/lpetool"); +#endif // WITH_LPETOOL +} + +static void _inkscape_fill_gtk(const gchar *path, GHashTable *t) +{ + const gchar *dir_entry; + GDir *dir = g_dir_open(path, 0, NULL); + + if (!dir) + return; + + while ((dir_entry = g_dir_read_name(dir))) { + gchar *filename = g_build_filename(path, dir_entry, "gtk-3.0", "gtk.css", NULL); + + if (g_file_test(filename, G_FILE_TEST_IS_REGULAR) && !g_hash_table_contains(t, dir_entry)) + g_hash_table_add(t, g_strdup(dir_entry)); + + g_free(filename); + } + + g_dir_close(dir); +} + +void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, + guint32 &colorsetwarning, guint32 &colorseterror) +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (themeiconname == prefs->getString("/theme/defaultIconTheme")) { + themeiconname = "hicolor"; + } + Glib::ustring prefix = ""; + if (prefs->getBool("/theme/darkTheme", false)) { + prefix = ".dark "; + } + Glib::ustring higlight = get_filename(ICONS, Glib::ustring(themeiconname + "/highlights.css").c_str(), false, true); + if (!higlight.empty()) { + std::ifstream ifs(higlight); + std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); + Glib::ustring result; + size_t startpos = content.find(prefix + ".base"); + size_t endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA base_color = Gdk::RGBA(result); + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".success"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA success_color = Gdk::RGBA(result); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".warning"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA warning_color = Gdk::RGBA(result); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".error"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA error_color = Gdk::RGBA(result); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + } + } +} + +void InkscapePreferences::resetIconsColors(bool themechange) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (!prefs->getBool("/theme/symbolicIcons", false)) { + _symbolic_base_colors.set_sensitive(false); + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + return; + } + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + // This colors are setted on style.css of inkscape + Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color(); + // This is a hack to fix a proble style is not updated enoght fast on + // chage from dark to bright themes + if (themechange) { + base_color = _symbolic_base_color.get_style_context()->get_background_color(); + } + Gdk::RGBA success_color = _symbolic_success_color.get_style_context()->get_color(); + Gdk::RGBA warning_color = _symbolic_warning_color.get_style_context()->get_color(); + Gdk::RGBA error_color = _symbolic_error_color.get_style_context()->get_color(); + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + guint32 colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + guint32 colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + guint32 colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); + _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); + prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + if (prefs->getBool("/theme/symbolicDefaultColors", true)) { + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + /* _complementary_colors->get_style_context()->add_class("disabled"); */ + } + changeIconsColors(); + } else { + _symbolic_base_color.setSensitive(true); + _symbolic_success_color.setSensitive(true); + _symbolic_warning_color.setSensitive(true); + _symbolic_error_color.setSensitive(true); + /* _complementary_colors->get_style_context()->remove_class("disabled"); */ + } +} + +void InkscapePreferences::resetIconsColorsWrapper() { resetIconsColors(false); } + +void InkscapePreferences::changeIconsColors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + Gtk::CssProvider::create(); + Glib::ustring css_str = ""; + if (prefs->getBool("/theme/symbolicIcons", false)) { + css_str = INKSCAPE.get_symbolic_colors(); + } + try { + INKSCAPE.colorizeprovider->load_from_data(css_str); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.colorizeprovider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +void InkscapePreferences::toggleSymbolic() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + if (window ) { + window->get_style_context()->add_class("symbolic"); + window->get_style_context()->remove_class("regular"); + } + _symbolic_base_colors.set_sensitive(true); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + } else { + if (window) { + window->get_style_context()->add_class("regular"); + window->get_style_context()->remove_class("symbolic"); + } + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + _symbolic_base_colors.set_sensitive(false); + } + INKSCAPE.signal_change_theme.emit(); +} + +void InkscapePreferences::themeChange() +{ + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool darktheme = prefs->getBool("/theme/preferDarkTheme", false); + Glib::ustring themename = prefs->getString("/theme/gtkTheme"); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + GtkSettings *settings = gtk_settings_get_default(); + g_object_set(settings, "gtk-theme-name", themename.c_str(), NULL); + g_object_set(settings, "gtk-application-prefer-dark-theme", darktheme, NULL); + bool dark = themename.find(":dark") != std::string::npos; + if (!dark) { + Glib::RefPtr stylecontext = window->get_style_context(); + Gdk::RGBA rgba; + bool background_set = stylecontext->lookup_color("theme_bg_color", rgba); + if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) { + dark = true; + } + } + Gtk::Widget *dialog_window = Glib::wrap(gobj()); + bool toggled = prefs->getBool("/theme/darkTheme", false) != dark; + if (dark) { + prefs->setBool("/theme/darkTheme", true); + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + prefs->setBool("/theme/darkTheme", false); + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + INKSCAPE.signal_change_theme.emit(); + resetIconsColors(toggled); + } +} + +void InkscapePreferences::symbolicThemeCheck() +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + bool symbolic = false; + GtkSettings *settings = gtk_settings_get_default(); + if (settings) { + if (themeiconname != "") { + g_object_set(settings, "gtk-icon-theme-name", themeiconname.c_str(), NULL); + } + } + if (prefs->getString("/theme/defaultIconTheme") != prefs->getString("/theme/iconTheme")) { + auto folders = get_foldernames(ICONS, { "application" }); + for (auto &folder : folders) { + auto path = folder; + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + if (folder == prefs->getString("/theme/iconTheme")) { +#ifdef _WIN32 + path += g_win32_locale_filename_from_utf8("/symbolic/actions"); +#else + path += "/symbolic/actions"; +#endif + std::vector symbolic_icons = get_filenames(path, { ".svg" }, {}); + if (symbolic_icons.size() > 0) { + symbolic = true; + symbolic_icons.clear(); + } + } + } + } else { + symbolic = true; + } + if (_symbolic_icons.get_parent()) { + if (!symbolic) { + _symbolic_icons.set_active(false); + _symbolic_icons.get_parent()->hide(); + _symbolic_base_colors.get_parent()->hide(); + _symbolic_base_color.get_parent()->get_parent()->hide(); + _symbolic_success_color.get_parent()->get_parent()->hide(); + } else { + _symbolic_icons.get_parent()->show(); + _symbolic_base_colors.get_parent()->show(); + _symbolic_base_color.get_parent()->get_parent()->show(); + _symbolic_success_color.get_parent()->get_parent()->show(); + } + } + if (symbolic) { + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + colorsetbase); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + _symbolic_error_color.init(_("Color for symbolic error icons:"), + "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + } +} +/* void sp_mix_colors(cairo_t *ct, int pos, SPColor a, SPColor b) +{ + double arcEnd=2*M_PI; + cairo_set_source_rgba(ct, 1, 1, 1, 1); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, a.v.c[0], a.v.c[1], a.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, b.v.c[0], b.v.c[1], b.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); +} + +Glib::RefPtr< Gdk::Pixbuf > sp_mix_colors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + SPColor success(colorsetsuccess); + SPColor warning(colorsetwarning); + SPColor error(colorseterror); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 82, 26); + cairo_t *ct = cairo_create(s); + // success + warning + sp_mix_colors(ct, 13, success, warning); + sp_mix_colors(ct, 41, success, error); + sp_mix_colors(ct, 69, warning, error); + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr sref = Cairo::RefPtr(new Cairo::Surface(s)); + Glib::RefPtr pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, 82, 26); + cairo_surface_destroy(s); + return pixbuf; +} */ + +void InkscapePreferences::changeIconsColor(guint32 /*color*/) +{ + changeIconsColors(); + /* _complementary_colors->set(sp_mix_colors()); */ +} + +void InkscapePreferences::initPageUI() +{ + Gtk::TreeModel::iterator iter_ui = this->AddPage(_page_ui, _("Interface"), PREFS_PAGE_UI); + _path_ui = _page_list.get_model()->get_path(iter_ui); + + Glib::ustring languages[] = {_("System default"), + _("Albanian (sq)"), _("Arabic (ar)"), _("Armenian (hy)"), _("Assamese (as)"), _("Azerbaijani (az)"), + _("Basque (eu)"), _("Belarusian (be)"), _("Bulgarian (bg)"), _("Bengali (bn)"), _("Bengali/Bangladesh (bn_BD)"), _("Bodo (brx)"), _("Breton (br)"), + _("Catalan (ca)"), _("Valencian Catalan (ca@valencia)"), _("Chinese/China (zh_CN)"), _("Chinese/Taiwan (zh_TW)"), _("Croatian (hr)"), _("Czech (cs)"), + _("Danish (da)"), _("Dogri (doi)"), _("Dutch (nl)"), _("Dzongkha (dz)"), + _("German (de)"), _("Greek (el)"), + _("English (en)"), _("English/Australia (en_AU)"), _("English/Canada (en_CA)"), _("English/Great Britain (en_GB)"), _("Esperanto (eo)"), _("Estonian (et)"), + _("Farsi (fa)"), _("Finnish (fi)"), _("French (fr)"), + _("Galician (gl)"), _("Gujarati (gu)"), + _("Hebrew (he)"), _("Hindi (hi)"), _("Hungarian (hu)"), + _("Icelandic (is)"), _("Indonesian (id)"), _("Irish (ga)"), _("Italian (it)"), + _("Japanese (ja)"), + _("Kannada (kn)"), _("Kashmiri in Perso-Arabic script (ks@aran)"), _("Kashmiri in Devanagari script (ks@deva)"), _("Khmer (km)"), _("Kinyarwanda (rw)"), _("Konkani (kok)"), _("Konkani in Latin script (kok@latin)"), _("Korean (ko)"), + _("Latvian (lv)"), _("Lithuanian (lt)"), + _("Macedonian (mk)"), _("Maithili (mai)"), _("Malayalam (ml)"), _("Manipuri (mni)"), _("Manipuri in Bengali script (mni@beng)"), _("Marathi (mr)"), _("Mongolian (mn)"), + _("Nepali (ne)"), _("Norwegian Bokmål (nb)"), _("Norwegian Nynorsk (nn)"), + _("Odia (or)"), + _("Panjabi (pa)"), _("Polish (pl)"), _("Portuguese (pt)"), _("Portuguese/Brazil (pt_BR)"), + _("Romanian (ro)"), _("Russian (ru)"), + _("Sanskrit (sa)"), _("Santali (sat)"), _("Santali in Devanagari script (sat@deva)"), _("Serbian (sr)"), _("Serbian in Latin script (sr@latin)"), + _("Sindhi (sd)"), _("Sindhi in Devanagari script (sd@deva)"), _("Slovak (sk)"), _("Slovenian (sl)"), _("Spanish (es)"), _("Spanish/Mexico (es_MX)"), _("Swedish (sv)"), + _("Tamil (ta)"), _("Telugu (te)"), _("Thai (th)"), _("Turkish (tr)"), + _("Ukrainian (uk)"), _("Urdu (ur)"), + _("Vietnamese (vi)")}; + Glib::ustring langValues[] = {"", + "sq", "ar", "hy", "as", "az", + "eu", "be", "bg", "bn", "bn_BD", "brx", "br", + "ca", "ca@valencia", "zh_CN", "zh_TW", "hr", "cs", + "da", "doi", "nl", "dz", + "de", "el", + "en", "en_AU", "en_CA", "en_GB", "eo", "et", + "fa", "fi", "fr", + "gl", "gu", + "he", "hi", "hu", + "is", "id", "ga", "it", + "ja", + "kn", "ks@aran", "ks@deva", "km", "rw", "kok", "kok@latin", "ko", + "lv", "lt", + "mk", "mai", "ml", "mni", "mni@beng", "mr", "mn", + "ne", "nb", "nn", + "or", + "pa", "pl", "pt", "pt_BR", + "ro", "ru", + "sa", "sat", "sat@deva", "sr", "sr@latin", + "sd", "sd@deva", "sk", "sl", "es", "es_MX", "sv", + "ta", "te", "th", "tr", + "uk", "ur", + "vi" }; + + { + // sorting languages according to translated name + int i = 0; + int j = 0; + int n = sizeof( languages ) / sizeof( Glib::ustring ); + Glib::ustring key_language; + Glib::ustring key_langValue; + for ( j = 1 ; j < n ; j++ ) { + key_language = languages[j]; + key_langValue = langValues[j]; + i = j-1; + while ( i >= 0 + && ( ( languages[i] > key_language + && langValues[i] != "" ) + || key_langValue == "" ) ) + { + languages[i+1] = languages[i]; + langValues[i+1] = langValues[i]; + i--; + } + languages[i+1] = key_language; + langValues[i+1] = key_langValue; + } + } + + _ui_languages.init( "/ui/language", languages, langValues, G_N_ELEMENTS(languages), languages[0]); + _page_ui.add_line( false, _("Language (requires restart):"), _ui_languages, "", + _("Set the language for menus and number formats"), false); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + _ui_colorsliders_top.init( _("Work-around color sliders not drawing"), "/options/workarounds/colorsontop", false); + _page_ui.add_line( false, "", _ui_colorsliders_top, "", + _("When on, will attempt to work around bugs in certain GTK themes drawing color sliders"), true); + + + _misc_recent.init("/options/maxrecentdocuments/value", 0.0, 1000.0, 1.0, 1.0, 1.0, true, false); + + Gtk::Button* reset_recent = Gtk::manage(new Gtk::Button(_("Clear list"))); + reset_recent->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_open_recent_clicked)); + + _page_ui.add_line( false, _("Maximum documents in Open _Recent:"), _misc_recent, "", + _("Set the maximum length of the Open Recent list in the File menu, or clear the list"), false, reset_recent); + + _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0); + _page_ui.add_line( false, _("_Zoom correction factor (in %):"), _ui_zoom_correction, "", + _("Adjust the slider until the length of the ruler on your screen matches its real length. This information is used when zooming to 1:1, 1:2, etc., to display objects in their true sizes"), true); + + _ui_partialdynamic.init( _("Enable dynamic relayout for incomplete sections"), "/options/workarounds/dynamicnotdone", false); + _page_ui.add_line( false, "", _ui_partialdynamic, "", + _("When on, will allow dynamic layout of components that are not completely finished being refactored"), true); + + /* show infobox */ + _show_filters_info_box.init( _("Show filter primitives infobox (requires restart)"), "/options/showfiltersinfobox/value", true); + _page_ui.add_line(false, "", _show_filters_info_box, "", + _("Show icons and descriptions for the filter primitives available at the filter effects dialog")); + + { + Glib::ustring dockbarstyleLabels[] = {_("Icons only"), _("Text only"), _("Icons and text")}; + int dockbarstyleValues[] = {0, 1, 2}; + + /* dockbar style */ + _dockbar_style.init( "/options/dock/dockbarstyle", dockbarstyleLabels, dockbarstyleValues, G_N_ELEMENTS(dockbarstyleLabels), 0); + _page_ui.add_line(false, _("Dockbar style (requires restart):"), _dockbar_style, "", + _("Selects whether the vertical bars on the dockbar will show text labels, icons, or both"), false); + + Glib::ustring switcherstyleLabels[] = {_("Text only"), _("Icons only"), _("Icons and text")}; /* see bug #1098437 */ + int switcherstyleValues[] = {0, 1, 2}; + + /* switcher style */ + _switcher_style.init( "/options/dock/switcherstyle", switcherstyleLabels, switcherstyleValues, G_N_ELEMENTS(switcherstyleLabels), 0); + _page_ui.add_line(false, _("Switcher style (requires restart):"), _switcher_style, "", + _("Selects whether the dockbar switcher will show text labels, icons, or both"), false); + } + + _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down (requires restart)"), "/options/yaxisdown", true); + _page_ui.add_line( false, "", _ui_yaxisdown, "", + _("When off, origin is at lower left corner and y-axis points up"), true); + + _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false); + _page_ui.add_line(false, "", _ui_rotationlock, "", + _("When enabled, common actions which normally rotate the canvas no longer do so by default"), true); + + + _mouse_grabsize.init("/options/grabsize/value", 1, 7, 1, 2, 3, 0); + _page_ui.add_line(false, _("_Handle size:"), _mouse_grabsize, "", + _("Set the relative size of node handles"), true); + + // Theme + _page_theme.add_group_header(_("Theme changes")); + { + using namespace Inkscape::IO::Resource; + GHashTable *t; + GHashTableIter iter; + gchar *theme, *path; + gchar **builtin_themes; + GList *list, *l; + guint i; + const gchar *const *dirs; + + t = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + /* Builtin themes */ + builtin_themes = g_resources_enumerate_children("/org/gtk/libgtk/theme", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); + for (i = 0; builtin_themes[i] != NULL; i++) { + if (g_str_has_suffix(builtin_themes[i], "/")) + g_hash_table_add(t, g_strndup(builtin_themes[i], strlen(builtin_themes[i]) - 1)); + } + g_strfreev(builtin_themes); + + path = g_build_filename(g_get_user_data_dir(), "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + path = g_build_filename(g_get_home_dir(), ".themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + dirs = g_get_system_data_dirs(); + for (i = 0; dirs[i]; i++) { + path = g_build_filename(dirs[i], "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + } + + list = NULL; + g_hash_table_iter_init(&iter, t); + while (g_hash_table_iter_next(&iter, (gpointer *)&theme, NULL)) + list = g_list_insert_sorted(list, theme, (GCompareFunc)strcmp); + + std::vector labels; + std::vector values; + for (l = list; l; l = l->next) { + theme = (gchar *)l->data; + labels.emplace_back(theme); + values.emplace_back(theme); + } + labels.emplace_back(_("Use system theme")); + values.push_back(prefs->getString("/theme/defaultTheme")); + g_list_free(list); + g_hash_table_destroy(t); + + _gtk_theme.init("/theme/gtkTheme", labels, values, "Adwaita"); + _page_theme.add_line(false, _("Change Gtk theme:"), _gtk_theme, "", "", false); + _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + } + _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("Open themes folder")); + _page_theme.add_line(true, _("User themes: "), _sys_user_themes_dir_copy, "", _("Location of the user’s themes"), true, Gtk::manage(new Gtk::Box())); + _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false); + _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true); + _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + // Icons + _page_theme.add_group_header(_("Display icons")); + { + using namespace Inkscape::IO::Resource; + auto folders = get_foldernames(ICONS, { "application" }); + std::vector labels; + std::vector values; + for (auto &folder : folders) { + // from https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path#8520871 + // Maybe we can link boost path utilities + // Remove directory if present. + // Do this before extension removal incase directory has a period character. + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + + labels.push_back(folder); + values.push_back(folder); + } + std::sort(labels.begin(), labels.end()); + std::sort(values.begin(), values.end()); + labels.erase(unique(labels.begin(), labels.end()), labels.end()); + values.erase(unique(values.begin(), values.end()), values.end()); + labels.emplace_back(_("Use system icons")); + values.push_back(prefs->getString("/theme/defaultIconTheme")); + _icon_theme.init("/theme/iconTheme", labels, values, "hicolor"); + _page_theme.add_line(false, _("Change icon theme:"), _icon_theme, "", "", false); + _icon_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::symbolicThemeCheck)); + _sys_user_icons_dir_copy.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_theme.add_line(true, _("User icons: "), _sys_user_icons_dir_copy, "", _("Location of the user’s icons"), true, Gtk::manage(new Gtk::Box())); + } + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + _symbolic_icons.init(_("Use symbolic icons"), "/theme/symbolicIcons", false); + _symbolic_icons.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::toggleSymbolic)); + _page_theme.add_line(true, "", _symbolic_icons, "", "", true); + _symbolic_base_colors.init(_("Use default colors for icons"), "/theme/symbolicDefaultColors", true); + _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); + _page_theme.add_line(true, "", _symbolic_base_colors, "", "", true); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + 0x2E3436ff); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + _symbolic_error_color.init(_("Color for symbolic error icons:"), "/theme/" + themeiconname + "/symbolicErrorColor", + 0xCC0000ff); + _symbolic_base_color.get_style_context()->add_class("system_base_color"); + _symbolic_success_color.get_style_context()->add_class("system_success_color"); + _symbolic_warning_color.get_style_context()->add_class("system_warning_color"); + _symbolic_error_color.get_style_context()->add_class("system_error_color"); + _symbolic_base_color.get_style_context()->add_class("symboliccolors"); + _symbolic_success_color.get_style_context()->add_class("symboliccolors"); + _symbolic_warning_color.get_style_context()->add_class("symboliccolors"); + _symbolic_error_color.get_style_context()->add_class("symboliccolors"); + _symbolic_base_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_warning_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_success_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_error_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + /* _complementary_colors = Gtk::manage(new Gtk::Image()); */ + Gtk::Box *icon_buttons = Gtk::manage(new Gtk::Box()); + icon_buttons->pack_start(_symbolic_base_color, true, true, 4); + _page_theme.add_line(false, "", *icon_buttons, _("Icon color"), + _("Base color for icons. Some icons changes need reload"), false); + Gtk::Box *icon_buttons_hight = Gtk::manage(new Gtk::Box()); + icon_buttons_hight->pack_start(_symbolic_success_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_warning_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_error_color, true, true, 4); + /* icon_buttons_hight->pack_start(*_complementary_colors, true, true, 4); */ + _page_theme.add_line(false, "", *icon_buttons_hight, _("Highlights"), + _("Highlights colors, some symbolic icon themes use it. Some icons changes need reload"), + false); + Gtk::Box *icon_buttons_def = Gtk::manage(new Gtk::Box()); + resetIconsColors(); + changeIconsColor(0xffffffff); + _page_theme.add_line(false, "", *icon_buttons_def, "", + _("Reset theme colors, some symbolic icon themes use it. Some icons changes need reload"), + false); + { + Glib::ustring sizeLabels[] = { C_("Icon size", "Larger"), C_("Icon size", "Large"), C_("Icon size", "Small"), + C_("Icon size", "Smaller") }; + int sizeValues[] = { 3, 2, 0, 1 }; + // "Larger" is 3 to not break existing preference files. Should fix in GTK3 + + _misc_small_tools.init("/toolbox/tools/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Toolbox icon size:"), _misc_small_tools, "", + _("Set the size for the tool icons (requires restart)"), false); + + _misc_small_toolbar.init("/toolbox/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Control bar icon size:"), _misc_small_toolbar, "", + _("Set the size for the icons in tools' control bars to use (requires restart)"), false); + + _misc_small_secondary.init("/toolbox/secondary", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 1); + _page_theme.add_line(false, _("Secondary toolbar icon size:"), _misc_small_secondary, "", + _("Set the size for the icons in secondary toolbars to use (requires restart)"), false); + } + { + Glib::ustring menu_icons_labels[] = {_("Yes"), _("No"), _("Theme decides")}; + int menu_icons_values[] = {1, -1, 0}; + _menu_icons.init("/theme/menuIcons", menu_icons_labels, menu_icons_values, G_N_ELEMENTS(menu_icons_labels), 0); + _page_theme.add_line(false, _("Show icons in menus:"), _menu_icons, "", + _("You can either enable or disable all icons in menus. By default the theme determines which icons to display by using the 'show-icons' attribute in its 'menus.xml' file. (requires restart)"), false); + } + + this->AddPage(_page_theme, _("Theme"), iter_ui, PREFS_PAGE_UI_THEME); + symbolicThemeCheck(); + // Windows + _win_save_geom.init ( _("Save and restore window geometry for each document"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_FILE, true, nullptr); + _win_save_geom_prefs.init ( _("Remember and use last window's geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_LAST, false, &_win_save_geom); + _win_save_geom_off.init ( _("Don't save window geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE, false, &_win_save_geom); + + _win_save_dialog_pos_on.init ( _("Save and restore dialogs status"), "/options/savedialogposition/value", 1, true, nullptr); + _win_save_dialog_pos_off.init ( _("Don't save dialogs status"), "/options/savedialogposition/value", 0, false, &_win_save_dialog_pos_on); + + _win_dockable.init ( _("Dockable"), "/options/dialogtype/value", 1, true, nullptr); + _win_floating.init ( _("Floating"), "/options/dialogtype/value", 0, false, &_win_dockable); + + + _win_native.init ( _("Native open/save dialogs"), "/options/desktopintegration/value", 1, true, nullptr); + _win_gtk.init ( _("GTK open/save dialogs"), "/options/desktopintegration/value", 0, false, &_win_native); + + _win_hide_task.init ( _("Dialogs are hidden in taskbar"), "/options/dialogsskiptaskbar/value", true); + _win_save_viewport.init ( _("Save and restore documents viewport"), "/options/savedocviewport/value", true); + _win_zoom_resize.init ( _("Zoom when window is resized"), "/options/stickyzoom/value", false); + _win_ontop_none.init ( C_("Dialog on top", "None"), "/options/transientpolicy/value", 0, false, nullptr); + _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", 1, true, &_win_ontop_none); + _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", 2, false, &_win_ontop_none); + + { + Glib::ustring defaultSizeLabels[] = {C_("Window size", "Default"), + C_("Window size", "Small"), + C_("Window size", "Large"), + C_("Window size", "Maximized")}; + int defaultSizeValues[] = {PREFS_WINDOW_SIZE_NATURAL, + PREFS_WINDOW_SIZE_SMALL, + PREFS_WINDOW_SIZE_LARGE, + PREFS_WINDOW_SIZE_MAXIMIZED}; + + _win_default_size.init( "/options/defaultwindowsize/value", defaultSizeLabels, defaultSizeValues, G_N_ELEMENTS(defaultSizeLabels), PREFS_WINDOW_SIZE_NATURAL); + _page_windows.add_line( false, _("Default window size:"), _win_default_size, "", + _("Set the default window size"), false); + } + + _page_windows.add_group_header( _("Saving window geometry (size and position)")); + _page_windows.add_line( true, "", _win_save_geom_off, "", + _("Let the window manager determine placement of all windows")); + _page_windows.add_line( true, "", _win_save_geom_prefs, "", + _("Remember and use the last window's geometry (saves geometry to user preferences)")); + _page_windows.add_line( true, "", _win_save_geom, "", + _("Save and restore window geometry for each document (saves geometry in the document)")); + + _page_windows.add_group_header( _("Saving dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_off, "", + _("Don't save dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_on, "", + _("Save and restore dialogs status (the last open windows dialogs are saved when it closes)")); + + + + _page_windows.add_group_header( _("Dialog behavior (requires restart)")); + _page_windows.add_line( true, "", _win_dockable, "", + _("Dockable")); + _page_windows.add_line( true, "", _win_floating, "", + _("Floating")); +#ifdef _WIN32 + _page_windows.add_group_header( _("Desktop integration")); + _page_windows.add_line( true, "", _win_native, "", + _("Use Windows like open and save dialogs")); + _page_windows.add_line( true, "", _win_gtk, "", + _("Use GTK open and save dialogs ")); +#endif + +#ifndef _WIN32 // non-Win32 special code to enable transient dialogs + _page_windows.add_group_header( _("Dialogs on top:")); + + _page_windows.add_line( true, "", _win_ontop_none, "", + _("Dialogs are treated as regular windows")); + _page_windows.add_line( true, "", _win_ontop_normal, "", + _("Dialogs stay on top of document windows")); + _page_windows.add_line( true, "", _win_ontop_agressive, "", + _("Same as Normal but may work better with some window managers")); +#endif + + _page_windows.add_group_header( _("Miscellaneous")); +#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs + _page_windows.add_line( true, "", _win_hide_task, "", + _("Whether dialog windows are to be hidden in the window manager taskbar")); +#endif + _page_windows.add_line( true, "", _win_zoom_resize, "", + _("Zoom drawing when document window is resized, to keep the same area visible (this is the default which can be changed in any window using the button above the right scrollbar)")); + _page_windows.add_line( true, "", _win_save_viewport, "", + _("Save documents viewport (zoom and panning position). Useful to turn off when sharing version controlled files.")); + this->AddPage(_page_windows, _("Windows"), iter_ui, PREFS_PAGE_UI_WINDOWS); + + // Grids + _page_grids.add_group_header( _("Line color when zooming out")); + + _grids_no_emphasize_on_zoom.init( _("Minor grid line color"), "/options/grids/no_emphasize_when_zoomedout", 1, true, nullptr); + _page_grids.add_line( true, "", _grids_no_emphasize_on_zoom, "", _("The gridlines will be shown in minor grid line color"), false); + _grids_emphasize_on_zoom.init( _("Major grid line color"), "/options/grids/no_emphasize_when_zoomedout", 0, false, &_grids_no_emphasize_on_zoom); + _page_grids.add_line( true, "", _grids_emphasize_on_zoom, "", _("The gridlines will be shown in major grid line color"), false); + + _page_grids.add_group_header( _("Default grid settings")); + + _page_grids.add_line( true, "", _grids_notebook, "", "", false); + _grids_notebook.append_page(_grids_xy, CanvasGrid::getName( GRID_RECTANGULAR )); + _grids_notebook.append_page(_grids_axonom, CanvasGrid::getName( GRID_AXONOMETRIC )); + _grids_xy_units.init("/options/grids/xy/units"); + _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false); + _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_x.set_digits(5); + _grids_xy_origin_y.set_digits(5); + _grids_xy.add_line( false, _("Origin X:"), _grids_xy_origin_x, "", _("X coordinate of grid origin"), false); + _grids_xy.add_line( false, _("Origin Y:"), _grids_xy_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_xy_spacing_x.init("/options/grids/xy/spacing_x", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_y.init("/options/grids/xy/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_x.set_digits(5); + _grids_xy_spacing_y.set_digits(5); + _grids_xy.add_line( false, _("Spacing X:"), _grids_xy_spacing_x, "", _("Distance between vertical grid lines"), false); + _grids_xy.add_line( false, _("Spacing Y:"), _grids_xy_spacing_y, "", _("Distance between horizontal grid lines"), false); + + _grids_xy_color.init(_("Minor grid line color:"), "/options/grids/xy/color", GRID_DEFAULT_COLOR); + _grids_xy.add_line( false, _("Minor grid line color:"), _grids_xy_color, "", _("Color used for normal grid lines"), false); + _grids_xy_empcolor.init(_("Major grid line color:"), "/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_xy.add_line( false, _("Major grid line color:"), _grids_xy_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_xy_empspacing.init("/options/grids/xy/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_xy.add_line( false, _("Major grid line every:"), _grids_xy_empspacing, "", "", false); + _grids_xy_dotted.init( _("Show dots instead of lines"), "/options/grids/xy/dotted", false); + _grids_xy.add_line( false, "", _grids_xy_dotted, "", _("If set, display dots at gridpoints instead of gridlines"), false); + + // CanvasAxonomGrid properties: + _grids_axonom_units.init("/options/grids/axonom/units"); + _grids_axonom.add_line( false, _("Grid units:"), _grids_axonom_units, "", "", false); + _grids_axonom_origin_x.init("/options/grids/axonom/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_y.init("/options/grids/axonom/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_x.set_digits(5); + _grids_axonom_origin_y.set_digits(5); + _grids_axonom.add_line( false, _("Origin X:"), _grids_axonom_origin_x, "", _("X coordinate of grid origin"), false); + _grids_axonom.add_line( false, _("Origin Y:"), _grids_axonom_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_axonom_spacing_y.init("/options/grids/axonom/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_axonom_spacing_y.set_digits(5); + _grids_axonom.add_line( false, _("Spacing Y:"), _grids_axonom_spacing_y, "", _("Base length of z-axis"), false); + _grids_axonom_angle_x.init("/options/grids/axonom/angle_x", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom_angle_z.init("/options/grids/axonom/angle_z", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom.add_line( false, _("Angle X:"), _grids_axonom_angle_x, "", _("Angle of x-axis"), false); + _grids_axonom.add_line( false, _("Angle Z:"), _grids_axonom_angle_z, "", _("Angle of z-axis"), false); + _grids_axonom_color.init(_("Minor grid line color:"), "/options/grids/axonom/color", GRID_DEFAULT_COLOR); + _grids_axonom.add_line( false, _("Minor grid line color:"), _grids_axonom_color, "", _("Color used for normal grid lines"), false); + _grids_axonom_empcolor.init(_("Major grid line color:"), "/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_axonom.add_line( false, _("Major grid line color:"), _grids_axonom_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_axonom_empspacing.init("/options/grids/axonom/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_axonom.add_line( false, _("Major grid line every:"), _grids_axonom_empspacing, "", "", false); + + this->AddPage(_page_grids, _("Grids"), iter_ui, PREFS_PAGE_UI_GRIDS); + + initKeyboardShortcuts(iter_ui); +} + +#if defined(HAVE_LIBLCMS2) +static void profileComboChanged( Gtk::ComboBoxText* combo ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int rowNum = combo->get_active_row_number(); + if ( rowNum < 1 ) { + prefs->setString("/options/displayprofile/uri", ""); + } else { + Glib::ustring active = combo->get_active_text(); + + Glib::ustring path = CMSSystem::getPathForProfile(active); + if ( !path.empty() ) { + prefs->setString("/options/displayprofile/uri", path); + } + } +} + +static void proofComboChanged( Gtk::ComboBoxText* combo ) +{ + Glib::ustring active = combo->get_active_text(); + Glib::ustring path = CMSSystem::getPathForProfile(active); + + if ( !path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/uri", path); + } +} + +static void gamutColorChanged( Gtk::ColorButton* btn ) { + auto rgba = btn->get_rgba(); + auto r = rgba.get_red_u(); + auto g = rgba.get_green_u(); + auto b = rgba.get_blue_u(); + + gchar* tmp = g_strdup_printf("#%02x%02x%02x", (r >> 8), (g >> 8), (b >> 8) ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/gamutcolor", tmp); + g_free(tmp); +} +#endif // defined(HAVE_LIBLCMS2) + +void InkscapePreferences::initPageIO() +{ + Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_IO); + _path_io = _page_list.get_model()->get_path(iter_io); + + _save_use_current_dir.init( _("Use current directory for \"Save As ...\""), "/dialogs/save_as/use_current_dir", true); + _page_io.add_line( false, "", _save_use_current_dir, "", + _("When this option is on, the \"Save as...\" and \"Save a Copy...\" dialogs will always open in the directory where the currently open document is; when it's off, each will open in the directory where you last saved a file using it"), true); + + _misc_comment.init( _("Add label comments to printing output"), "/printing/debug/show-label-comments", false); + _page_io.add_line( false, "", _misc_comment, "", + _("When on, a comment will be added to the raw print output, marking the rendered output for an object with its label"), true); + + _misc_default_metadata.init( _("Add default metadata to new documents"), "/metadata/addToNewFile", false); + _page_io.add_line( false, "", _misc_default_metadata, "", + _("Add default metadata to new documents. Default metadata can be set from Document Properties->Metadata."), true); + + // Input devices options + _mouse_sens.init ( "/options/cursortolerance/value", 0.0, 30.0, 1.0, 1.0, 8.0, true, false); + _page_mouse.add_line( false, _("_Grab sensitivity:"), _mouse_sens, _("pixels (requires restart)"), + _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false); + _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 20.0, 1.0, 1.0, 4.0, true, false); + _page_mouse.add_line( false, _("_Click/drag threshold:"), _mouse_thres, _("pixels"), + _("Maximum mouse drag (in screen pixels) which is considered a click, not a drag"), false); + + _mouse_use_ext_input.init( _("Use pressure-sensitive tablet (requires restart)"), "/options/useextinput/value", true); + _page_mouse.add_line(false, "",_mouse_use_ext_input, "", + _("Use the capabilities of a tablet or other pressure-sensitive device. Disable this only if you have problems with the tablet (you can still use it as a mouse)")); + + _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device (requires restart)"), "/options/switchonextinput/value", false); + _page_mouse.add_line(false, "",_mouse_switch_on_ext_input, "", + _("Change tool as different devices are used on the tablet (pen, eraser, mouse)")); + this->AddPage(_page_mouse, _("Input devices"), iter_io, PREFS_PAGE_IO_MOUSE); + + // SVG output options + _svgoutput_usenamedcolors.init( _("Use named colors"), "/options/svgoutput/usenamedcolors", false); + _page_svgoutput.add_line( false, "", _svgoutput_usenamedcolors, "", _("If set, write the CSS name of the color when available (e.g. 'red' or 'magenta') instead of the numeric value"), false); + + _page_svgoutput.add_group_header( _("XML formatting")); + + _svgoutput_inlineattrs.init( _("Inline attributes"), "/options/svgoutput/inlineattrs", false); + _page_svgoutput.add_line( true, "", _svgoutput_inlineattrs, "", _("Put attributes on the same line as the element tag"), false); + + _svgoutput_indent.init("/options/svgoutput/indent", 0.0, 1000.0, 1.0, 2.0, 2.0, true, false); + _page_svgoutput.add_line( true, _("_Indent, spaces:"), _svgoutput_indent, "", _("The number of spaces to use for indenting nested elements; set to 0 for no indentation"), false); + + _page_svgoutput.add_group_header( _("Path data")); + + int const numPathstringFormat = 3; + Glib::ustring pathstringFormatLabels[numPathstringFormat] = {_("Absolute"), _("Relative"), _("Optimized")}; + int pathstringFormatValues[numPathstringFormat] = {0, 1, 2}; + + _svgoutput_pathformat.init("/options/svgoutput/pathstring_format", pathstringFormatLabels, pathstringFormatValues, numPathstringFormat, 2); + _page_svgoutput.add_line( true, _("Path string format:"), _svgoutput_pathformat, "", _("Path data should be written: only with absolute coordinates, only with relative coordinates, or optimized for string length (mixed absolute and relative coordinates)"), false); + + _svgoutput_forcerepeatcommands.init( _("Force repeat commands"), "/options/svgoutput/forcerepeatcommands", false); + _page_svgoutput.add_line( true, "", _svgoutput_forcerepeatcommands, "", _("Force repeating of the same path command (for example, 'L 1,2 L 3,4' instead of 'L 1,2 3,4')"), false); + + _page_svgoutput.add_group_header( _("Numbers")); + + _svgoutput_numericprecision.init("/options/svgoutput/numericprecision", 1.0, 16.0, 1.0, 2.0, 8.0, true, false); + _page_svgoutput.add_line( true, _("_Numeric precision:"), _svgoutput_numericprecision, "", _("Significant figures of the values written to the SVG file"), false); + + _svgoutput_minimumexponent.init("/options/svgoutput/minimumexponent", -32.0, -1, 1.0, 2.0, -8.0, true, false); + _page_svgoutput.add_line( true, _("Minimum _exponent:"), _svgoutput_minimumexponent, "", _("The smallest number written to SVG is 10 to the power of this exponent; anything smaller is written as zero"), false); + + /* Code to add controls for attribute checking options */ + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Improper Attributes Actions")); + + _svgoutput_attrwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_attributes_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_attrwarn, "", _("Print warning if invalid or non-useful attributes found. Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_attrremove.init( _("Remove attributes"), "/options/svgoutput/incorrect_attributes_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_attrremove, "", _("Delete invalid or non-useful attributes from element tag"), false); + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Inappropriate Style Properties Actions")); + + _svgoutput_stylepropwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_style_properties_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropwarn, "", _("Print warning if inappropriate style properties found (i.e. 'font-family' set on a ). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_stylepropremove.init( _("Remove style properties"), "/options/svgoutput/incorrect_style_properties_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropremove, "", _("Delete inappropriate style properties"), false); + + /* Add default or inherited style properties options */ + _page_svgoutput.add_group_header( _("Non-useful Style Properties Actions")); + + _svgoutput_styledefaultswarn.init( _("Print warnings"), "/options/svgoutput/style_defaults_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultswarn, "", _("Print warning if redundant style properties found (i.e. if a property has the default value and a different value is not inherited or if value is the same as would be inherited). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_styledefaultsremove.init( _("Remove style properties"), "/options/svgoutput/style_defaults_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultsremove, "", _("Delete redundant style properties"), false); + + _page_svgoutput.add_group_header( _("Check Attributes and Style Properties on")); + + _svgoutput_check_reading.init( _("Reading"), "/options/svgoutput/check_on_reading", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_reading, "", _("Check attributes and style properties on reading in SVG files (including those internal to Inkscape which will slow down startup)"), false); + _svgoutput_check_editing.init( _("Editing"), "/options/svgoutput/check_on_editing", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_editing, "", _("Check attributes and style properties while editing SVG files (may slow down Inkscape, mostly useful for debugging)"), false); + _svgoutput_check_writing.init( _("Writing"), "/options/svgoutput/check_on_writing", true); + _page_svgoutput.add_line( true, "", _svgoutput_check_writing, "", _("Check attributes and style properties on writing out SVG files"), false); + + this->AddPage(_page_svgoutput, _("SVG output"), iter_io, PREFS_PAGE_IO_SVGOUTPUT); + + // SVG Export Options ========================================== + + // SVG 2 Fallbacks + _page_svgexport.add_group_header( _("SVG 2")); + _svgexport_insert_text_fallback.init( _("Insert SVG 1.1 fallback in text."), "/options/svgexport/text_insertfallback", true ); + _svgexport_insert_mesh_polyfill.init( _("Insert Mesh Gradient JavaScript polyfill."), "/options/svgexport/mesh_insertpolyfill", true ); + _svgexport_insert_hatch_polyfill.init( _("Insert Hatch Paint Server JavaScript polyfill."), "/options/svgexport/hatch_insertpolyfill", true ); + + _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds JavaScript polyfill to render meshes."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds JavaScript polyfill to render hatches (linear and absolute paths)."), false); + + // SVG Export Options (SVG 2 -> SVG 1) + _page_svgexport.add_group_header( _("SVG 2 to SVG 1.1")); + + _svgexport_remove_marker_auto_start_reverse.init( _("Replace markers with 'auto_start_reverse'."), "/options/svgexport/marker_autostartreverse", false); + _svgexport_remove_marker_context_paint.init( _("Replace markers using 'context_paint' or 'context_fill'."), "/options/svgexport/marker_contextpaint", false); + + _page_svgexport.add_line( false, "", _svgexport_remove_marker_auto_start_reverse, "", _("SVG 2 allows markers to automatically be reversed at start of path."), false); + _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match stroke color."), false); + + this->AddPage(_page_svgexport, _("SVG export"), iter_io, PREFS_PAGE_IO_SVGEXPORT); + + + // CMS options + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const numIntents = 4; + /* TRANSLATORS: see http://www.newsandtech.com/issues/2004/03-04/pt/03-04_rendering.htm */ + Glib::ustring intentLabels[numIntents] = {_("Perceptual"), _("Relative Colorimetric"), _("Saturation"), _("Absolute Colorimetric")}; + int intentValues[numIntents] = {0, 1, 2, 3}; + +#if !defined(HAVE_LIBLCMS2) + Gtk::Label* lbl = new Gtk::Label(_("(Note: Color management has been disabled in this build)")); + _page_cms.add_line( false, "", *lbl, "", "", true); +#endif // !defined(HAVE_LIBLCMS2) + + _page_cms.add_group_header( _("Display adjustment")); + + Glib::ustring tmpStr; + for (auto &profile: ColorProfile::getBaseProfileDirs()) { + gchar* part = g_strdup_printf( "\n%s", profile.filename.c_str() ); + tmpStr += part; + g_free(part); + } + + gchar* profileTip = g_strdup_printf(_("The ICC profile to use to calibrate display output.\nSearched directories:%s"), tmpStr.c_str()); + _page_cms.add_line( true, _("Display profile:"), _cms_display_profile, "", + profileTip, false); + g_free(profileTip); + profileTip = nullptr; + + _cms_from_display.init( _("Retrieve profile from display"), "/options/displayprofile/from_display", false); + _page_cms.add_line( true, "", _cms_from_display, "", +#ifdef GDK_WINDOWING_X11 + _("Retrieve profiles from those attached to displays via XICC"), false); +#else + _("Retrieve profiles from those attached to displays"), false); +#endif // GDK_WINDOWING_X11 + + + _cms_intent.init("/options/displayprofile/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Display rendering intent:"), _cms_intent, "", + _("The rendering intent to use to calibrate display output"), false); + + _page_cms.add_group_header( _("Proofing")); + + _cms_softproof.init( _("Simulate output on screen"), "/options/softproof/enable", false); + _page_cms.add_line( true, "", _cms_softproof, "", + _("Simulates output of target device"), false); + + _cms_gamutwarn.init( _("Mark out of gamut colors"), "/options/softproof/gamutwarn", false); + _page_cms.add_line( true, "", _cms_gamutwarn, "", + _("Highlights colors that are out of gamut for the target device"), false); + + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + + Gdk::RGBA tmpColor( colorStr.empty() ? "#00ff00" : colorStr); + _cms_gamutcolor.set_rgba( tmpColor ); + + _page_cms.add_line( true, _("Out of gamut warning color:"), _cms_gamutcolor, "", + _("Selects the color used for out of gamut warning"), false); + + _page_cms.add_line( true, _("Device profile:"), _cms_proof_profile, "", + _("The ICC profile to use to simulate device output"), false); + + _cms_proof_intent.init("/options/softproof/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Device rendering intent:"), _cms_proof_intent, "", + _("The rendering intent to use to calibrate device output"), false); + + _cms_proof_blackpoint.init( _("Black point compensation"), "/options/softproof/bpc", false); + _page_cms.add_line( true, "", _cms_proof_blackpoint, "", + _("Enables black point compensation"), false); + + _cms_proof_preserveblack.init( _("Preserve black"), "/options/softproof/preserveblack", false); + +#if !defined(HAVE_LIBLCMS2) + _page_cms.add_line( true, "", _cms_proof_preserveblack, +#if defined(cmsFLAGS_PRESERVEBLACK) + "", +#else + _("(LittleCMS 1.15 or later required)"), +#endif // defined(cmsFLAGS_PRESERVEBLACK) + _("Preserve K channel in CMYK -> CMYK transforms"), false); +#endif // !defined(HAVE_LIBLCMS2) + +#if !defined(cmsFLAGS_PRESERVEBLACK) + _cms_proof_preserveblack.set_sensitive( false ); +#endif // !defined(cmsFLAGS_PRESERVEBLACK) + + +#if defined(HAVE_LIBLCMS2) + { + std::vector names = ::Inkscape::CMSSystem::getDisplayNames(); + Glib::ustring current = prefs->getString( "/options/displayprofile/uri" ); + + gint index = 0; + _cms_display_profile.append(_("")); + index++; + for (auto & name : names) { + _cms_display_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_display_profile.set_active(index); + } + index++; + } + if ( current.empty() ) { + _cms_display_profile.set_active(0); + } + + names = ::Inkscape::CMSSystem::getSoftproofNames(); + current = prefs->getString("/options/softproof/uri"); + index = 0; + for (auto & name : names) { + _cms_proof_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_proof_profile.set_active(index); + } + index++; + } + } + + _cms_gamutcolor.signal_color_set().connect( sigc::bind( sigc::ptr_fun(gamutColorChanged), &_cms_gamutcolor) ); + + _cms_display_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(profileComboChanged), &_cms_display_profile) ); + _cms_proof_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(proofComboChanged), &_cms_proof_profile) ); +#else + // disable it, but leave it visible + _cms_intent.set_sensitive( false ); + _cms_display_profile.set_sensitive( false ); + _cms_from_display.set_sensitive( false ); + _cms_softproof.set_sensitive( false ); + _cms_gamutwarn.set_sensitive( false ); + _cms_gamutcolor.set_sensitive( false ); + _cms_proof_intent.set_sensitive( false ); + _cms_proof_profile.set_sensitive( false ); + _cms_proof_blackpoint.set_sensitive( false ); + _cms_proof_preserveblack.set_sensitive( false ); +#endif // defined(HAVE_LIBLCMS2) + + this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS); + + // Autosave options + _save_autosave_enable.init( _("Enable autosave (requires restart)"), "/options/autosave/enable", true); + _page_autosave.add_line(false, "", _save_autosave_enable, "", _("Automatically save the current document(s) at a given interval, thus minimizing loss in case of a crash"), false); + _save_autosave_path.init("/options/autosave/path", true); + if (prefs->getString("/options/autosave/path").empty()) { + // Show the default fallback "tmp dir" if autosave path is not set. + _save_autosave_path.set_text(Glib::build_filename(Glib::get_user_cache_dir(), "inkscape")); + } + _page_autosave.add_line(false, C_("Filesystem", "Autosave _directory:"), _save_autosave_path, "", _("The directory where autosaves will be written. This should be an absolute path (starts with / on UNIX or a drive letter such as C: on Windows). "), false); + _save_autosave_interval.init("/options/autosave/interval", 1.0, 10800.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Interval (in minutes):"), _save_autosave_interval, "", _("Interval (in minutes) at which document will be autosaved"), false); + _save_autosave_max.init("/options/autosave/max", 1.0, 100.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Maximum number of autosaves:"), _save_autosave_max, "", _("Maximum number of autosaved files; use this to limit the storage space used"), false); + + /* When changing the interval or enabling/disabling the autosave function, + * update our running configuration + * + * FIXME! + * AutoSave::restart() should be called AFTER the values have been changed + * (which cannot be guaranteed from here) - use a PrefObserver somewhere. + */ + _save_autosave_enable.signal_toggled( ).connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + _save_autosave_interval.signal_changed().connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + + this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE); +} + +void InkscapePreferences::initPageBehavior() +{ + Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_BEHAVIOR); + _path_behavior = _page_list.get_model()->get_path(iter_behavior); + + _misc_simpl.init("/options/simplifythreshold/value", 0.0001, 1.0, 0.0001, 0.0010, 0.0010, false, false); + _page_behavior.add_line( false, _("_Simplification threshold:"), _misc_simpl, "", + _("How strong is the Node tool's Simplify command by default. If you invoke this command several times in quick succession, it will act more and more aggressively; invoking it again after a pause restores the default threshold."), false); + + _markers_color_stock.init ( _("Color stock markers the same color as object"), "/options/markers/colorStockMarkers", true); + _markers_color_custom.init ( _("Color custom markers the same color as object"), "/options/markers/colorCustomMarkers", false); + _markers_color_update.init ( _("Update marker color when object color changes"), "/options/markers/colorUpdateMarkers", true); + + // Selecting options + _sel_all.init ( _("Select in all layers"), "/options/kbselection/inlayer", PREFS_SELECTION_ALL, false, nullptr); + _sel_current.init ( _("Select only within current layer"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER, true, &_sel_all); + _sel_recursive.init ( _("Select in current layer and sublayers"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER_RECURSIVE, false, &_sel_all); + _sel_hidden.init ( _("Ignore hidden objects and layers"), "/options/kbselection/onlyvisible", true); + _sel_locked.init ( _("Ignore locked objects and layers"), "/options/kbselection/onlysensitive", true); + _sel_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", true); + + _page_select.add_line( false, "", _sel_layer_deselects, "", + _("Uncheck this to be able to keep the current objects selected when the current layer changes")); + + _page_select.add_group_header( _("Ctrl+A, Tab, Shift+Tab")); + _page_select.add_line( true, "", _sel_all, "", + _("Make keyboard selection commands work on objects in all layers")); + _page_select.add_line( true, "", _sel_current, "", + _("Make keyboard selection commands work on objects in current layer only")); + _page_select.add_line( true, "", _sel_recursive, "", + _("Make keyboard selection commands work on objects in current layer and all its sublayers")); + _page_select.add_line( true, "", _sel_hidden, "", + _("Uncheck this to be able to select objects that are hidden (either by themselves or by being in a hidden layer)")); + _page_select.add_line( true, "", _sel_locked, "", + _("Uncheck this to be able to select objects that are locked (either by themselves or by being in a locked layer)")); + + _sel_cycle.init ( _("Wrap when cycling objects in z-order"), "/options/selection/cycleWrap", true); + + _page_select.add_group_header( _("Alt+Scroll Wheel")); + _page_select.add_line( true, "", _sel_cycle, "", + _("Wrap around at start and end when cycling objects in z-order")); + + this->AddPage(_page_select, _("Selecting"), iter_behavior, PREFS_PAGE_BEHAVIOR_SELECTING); + + // Transforms options + _trans_scale_stroke.init ( _("Scale stroke width"), "/options/transform/stroke", true); + _trans_scale_corner.init ( _("Scale rounded corners in rectangles"), "/options/transform/rectcorners", false); + _trans_gradient.init ( _("Transform gradients"), "/options/transform/gradient", true); + _trans_pattern.init ( _("Transform patterns"), "/options/transform/pattern", false); + _trans_optimized.init ( _("Optimized"), "/options/preservetransform/value", 0, true, nullptr); + _trans_preserved.init ( _("Preserved"), "/options/preservetransform/value", 1, false, &_trans_optimized); + + _page_transforms.add_line( false, "", _trans_scale_stroke, "", + _("When scaling objects, scale the stroke width by the same proportion")); + _page_transforms.add_line( false, "", _trans_scale_corner, "", + _("When scaling rectangles, scale the radii of rounded corners")); + _page_transforms.add_line( false, "", _trans_gradient, "", + _("Move gradients (in fill or stroke) along with the objects")); + _page_transforms.add_line( false, "", _trans_pattern, "", + _("Move patterns (in fill or stroke) along with the objects")); + _page_transforms.add_group_header( _("Store transformation")); + _page_transforms.add_line( true, "", _trans_optimized, "", + _("If possible, apply transformation to objects without adding a transform= attribute")); + _page_transforms.add_line( true, "", _trans_preserved, "", + _("Always store transformation as a transform= attribute on objects")); + + this->AddPage(_page_transforms, _("Transforms"), iter_behavior, PREFS_PAGE_BEHAVIOR_TRANSFORMS); + + _dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true); + _page_dashes.add_line(false, "", _dash_scale, "", _("When changing stroke width, scale dash array")); + + this->AddPage(_page_dashes, _("Dashes"), iter_behavior, PREFS_PAGE_BEHAVIOR_DASHES); + + // Scrolling options + _scroll_wheel.init ( "/options/wheelscroll/value", 0.0, 1000.0, 1.0, 1.0, 40.0, true, false); + _page_scrolling.add_line( false, _("Mouse _wheel scrolls by:"), _scroll_wheel, _("pixels"), + _("One mouse wheel notch scrolls by this distance in screen pixels (horizontally with Shift)"), false); + _page_scrolling.add_group_header( _("Ctrl+arrows")); + _scroll_arrow_px.init ( "/options/keyscroll/value", 0.0, 1000.0, 1.0, 1.0, 10.0, true, false); + _page_scrolling.add_line( true, _("Sc_roll by:"), _scroll_arrow_px, _("pixels"), + _("Pressing Ctrl+arrow key scrolls by this distance (in screen pixels)"), false); + _scroll_arrow_acc.init ( "/options/scrollingacceleration/value", 0.0, 5.0, 0.01, 1.0, 0.35, false, false); + _page_scrolling.add_line( true, _("_Acceleration:"), _scroll_arrow_acc, "", + _("Pressing and holding Ctrl+arrow will gradually speed up scrolling (0 for no acceleration)"), false); + _page_scrolling.add_group_header( _("Autoscrolling")); + _scroll_auto_speed.init ( "/options/autoscrollspeed/value", 0.0, 5.0, 0.01, 1.0, 0.7, false, false); + _page_scrolling.add_line( true, _("_Speed:"), _scroll_auto_speed, "", + _("How fast the canvas autoscrolls when you drag beyond canvas edge (0 to turn autoscroll off)"), false); + _scroll_auto_thres.init ( "/options/autoscrolldistance/value", -600.0, 600.0, 1.0, 1.0, -10.0, true, false); + _page_scrolling.add_line( true, _("_Threshold:"), _scroll_auto_thres, _("pixels"), + _("How far (in screen pixels) you need to be from the canvas edge to trigger autoscroll; positive is outside the canvas, negative is within the canvas"), false); + _scroll_space.init ( _("Mouse move pans when Space is pressed"), "/options/spacebarpans/value", true); + _page_scrolling.add_line( true, "", _scroll_space, "", + _("When on, pressing and holding Space and dragging pans canvas")); + _wheel_zoom.init ( _("Mouse wheel zooms by default"), "/options/wheelzooms/value", false); + _page_scrolling.add_line( false, "", _wheel_zoom, "", + _("When on, mouse wheel zooms without Ctrl and scrolls canvas with Ctrl; when off, it zooms with Ctrl and scrolls without Ctrl")); + this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING); + + // Snapping options + _page_snapping.add_group_header( _("Snap defaults")); + + _snap_default.init( _("Enable snapping in new documents"), "/options/snapdefault/value", true); + _page_snapping.add_line( true, "", _snap_default, "", + _("Initial state of snapping in new documents and non-Inkscape SVGs. Snap status is subsequently saved per-document.")); + + _page_snapping.add_group_header( _("Snap indicator")); + + _snap_indicator.init( _("Enable snap indicator"), "/options/snapindicator/value", true); + _page_snapping.add_line( true, "", _snap_indicator, "", + _("After snapping, a symbol is drawn at the point that has snapped")); + + _snap_indicator.changed_signal.connect( sigc::mem_fun(_snap_persistence, &Gtk::Widget::set_sensitive) ); + + _snap_persistence.init("/options/snapindicatorpersistence/value", 0.1, 10, 0.1, 1, 2, 1); + _page_snapping.add_line( true, _("Snap indicator persistence (in seconds):"), _snap_persistence, "", + _("Controls how long the snap indicator message will be shown, before it disappears"), true); + + _page_snapping.add_group_header( _("What should snap")); + + _snap_closest_only.init( _("Only snap the node closest to the pointer"), "/options/snapclosestonly/value", false); + _page_snapping.add_line( true, "", _snap_closest_only, "", + _("Only try to snap the node that is initially closest to the mouse pointer")); + + _snap_weight.init("/options/snapweight/value", 0, 1, 0.1, 0.2, 0.5, 1); + _page_snapping.add_line( true, _("_Weight factor:"), _snap_weight, "", + _("When multiple snap solutions are found, then Inkscape can either prefer the closest transformation (when set to 0), or prefer the node that was initially the closest to the pointer (when set to 1)"), true); + + _snap_mouse_pointer.init( _("Snap the mouse pointer when dragging a constrained knot"), "/options/snapmousepointer/value", false); + _page_snapping.add_line( true, "", _snap_mouse_pointer, "", + _("When dragging a knot along a constraint line, then snap the position of the mouse pointer instead of snapping the projection of the knot onto the constraint line")); + + _page_snapping.add_group_header( _("Delayed snap")); + + _snap_delay.init("/options/snapdelay/value", 0, 1, 0.1, 0.2, 0, 1); + _page_snapping.add_line( true, _("Delay (in seconds):"), _snap_delay, "", + _("Postpone snapping as long as the mouse is moving, and then wait an additional fraction of a second. This additional delay is specified here. When set to zero or to a very small number, snapping will be immediate."), true); + + this->AddPage(_page_snapping, _("Snapping"), iter_behavior, PREFS_PAGE_BEHAVIOR_SNAPPING); + + // Steps options + _steps_arrow.init ( "/options/nudgedistance/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //nudgedistance is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("_Arrow keys move by:"), _steps_arrow, "", + _("Pressing an arrow key moves selected object(s) or node(s) by this distance"), false); + _steps_scale.init ( "/options/defaultscale/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //defaultscale is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("> and < _scale by:"), _steps_scale, "", + _("Pressing > or < scales selection up or down by this increment"), false); + _steps_inset.init ( "/options/defaultoffsetwidth/value", 0.0, 3000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + _page_steps.add_line( false, _("_Inset/Outset by:"), _steps_inset, "", + _("Inset and Outset commands displace the path by this distance"), false); + _steps_compass.init ( _("Compass-like display of angles"), "/options/compassangledisplay/value", true); + _page_steps.add_line( false, "", _steps_compass, "", + _("When on, angles are displayed with 0 at north, 0 to 360 range, positive clockwise; otherwise with 0 at east, -180 to 180 range, positive counterclockwise")); + int const num_items = 18; + Glib::ustring labels[num_items] = {"90", "60", "45", "36", "30", "22.5", "18", "15", "12", "10", "7.5", "6", "5", "3", "2", "1", "0.5", C_("Rotation angle", "None")}; + int values[num_items] = {2, 3, 4, 5, 6, 8, 10, 12, 15, 18, 24, 30, 36, 60, 90, 180, 360, 0}; + _steps_rot_snap.set_size_request(_sb_width); + _steps_rot_snap.init("/options/rotationsnapsperpi/value", labels, values, num_items, 12); + _page_steps.add_line( false, _("_Rotation snaps every:"), _steps_rot_snap, _("degrees"), + _("Rotating with Ctrl pressed snaps every that much degrees; also, pressing [ or ] rotates by this amount"), false); + _steps_rot_relative.init ( _("Relative snapping of guideline angles"), "/options/relativeguiderotationsnap/value", false); + _page_steps.add_line( false, "", _steps_rot_relative, "", + _("When on, the snap angles when rotating a guideline will be relative to the original angle")); + _steps_zoom.init ( "/options/zoomincrement/value", 101.0, 500.0, 1.0, 1.0, M_SQRT2, true, true); + _page_steps.add_line( false, _("_Zoom in/out by:"), _steps_zoom, _("%"), + _("Zoom tool click, +/- keys, and middle click zoom in and out by this multiplier"), false); + _middle_mouse_zoom.init ( _("Zoom with middle mouse click"), "/options/middlemousezoom/value", true); + _page_steps.add_line( true, "", _middle_mouse_zoom, "", + _("When on, clicking the middle mouse button (usually the mouse wheel) makes zoom.")); + _steps_rotate.init ( "/options/rotateincrement/value", 1, 90, 1.0, 5.0, 15, false, false); + _page_steps.add_line( false, _("_Rotate canvas by:"), _steps_rotate, _("degrees"), + _("Rotate canvas clockwise and counter-clockwise by this amount."), false); + this->AddPage(_page_steps, _("Steps"), iter_behavior, PREFS_PAGE_BEHAVIOR_STEPS); + + // Clones options + _clone_option_parallel.init ( _("Move in parallel"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_PARALLEL, true, nullptr); + _clone_option_stay.init ( _("Stay unmoved"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_UNMOVED, false, &_clone_option_parallel); + _clone_option_transform.init ( _("Move according to transform"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_NONE, false, &_clone_option_parallel); + _clone_option_unlink.init ( _("Are unlinked"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_UNLINK, true, nullptr); + _clone_option_delete.init ( _("Are deleted"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_DELETE, false, &_clone_option_unlink); + + _page_clones.add_group_header( _("Moving original: clones and linked offsets")); + _page_clones.add_line(true, "", _clone_option_parallel, "", + _("Clones are translated by the same vector as their original")); + _page_clones.add_line(true, "", _clone_option_stay, "", + _("Clones preserve their positions when their original is moved")); + _page_clones.add_line(true, "", _clone_option_transform, "", + _("Each clone moves according to the value of its transform= attribute; for example, a rotated clone will move in a different direction than its original")); + _page_clones.add_group_header( _("Deleting original: clones")); + _page_clones.add_line(true, "", _clone_option_unlink, "", + _("Orphaned clones are converted to regular objects")); + _page_clones.add_line(true, "", _clone_option_delete, "", + _("Orphaned clones are deleted along with their original")); + + _page_clones.add_group_header( _("Duplicating original+clones/linked offset")); + + _clone_relink_on_duplicate.init ( _("Relink duplicated clones"), "/options/relinkclonesonduplicate/value", false); + _page_clones.add_line(true, "", _clone_relink_on_duplicate, "", + _("When duplicating a selection containing both a clone and its original (possibly in groups), relink the duplicated clone to the duplicated original instead of the old original")); + + _page_clones.add_group_header( _("Unlinking clones")); + _clone_to_curves.init ( _("Path operations unlink clones"), "/options/pathoperationsunlink/value", true); + _page_clones.add_line(true, "", _clone_to_curves, "", + _("The following path operations will unlink clones: Stroke to path, Object to path, Boolean operations, Combine, Break apart")); + + //TRANSLATORS: Heading for the Inkscape Preferences "Clones" Page + this->AddPage(_page_clones, _("Clones"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLONES); + + // Clip paths and masks options + _mask_mask_on_top.init ( _("When applying, use the topmost selected object as clippath/mask"), "/options/maskobject/topmost", true); + _page_mask.add_line(false, "", _mask_mask_on_top, "", + _("Uncheck this to use the bottom selected object as the clipping path or mask")); + _mask_mask_remove.init ( _("Remove clippath/mask object after applying"), "/options/maskobject/remove", true); + _page_mask.add_line(false, "", _mask_mask_remove, "", + _("After applying, remove the object used as the clipping path or mask from the drawing")); + + _page_mask.add_group_header( _("Before applying")); + + _mask_grouping_none.init( _("Do not group clipped/masked objects"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE, true, nullptr); + _mask_grouping_separate.init( _("Put every clipped/masked object in its own group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_SEPARATE, false, &_mask_grouping_none); + _mask_grouping_all.init( _("Put all clipped/masked objects into one group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_ALL, false, &_mask_grouping_none); + + _page_mask.add_line(true, "", _mask_grouping_none, "", + _("Apply clippath/mask to every object")); + + _page_mask.add_line(true, "", _mask_grouping_separate, "", + _("Apply clippath/mask to groups containing single object")); + + _page_mask.add_line(true, "", _mask_grouping_all, "", + _("Apply clippath/mask to group containing all objects")); + + _page_mask.add_group_header( _("After releasing")); + + _mask_ungrouping.init ( _("Ungroup automatically created groups"), "/options/maskobject/ungrouping", true); + _page_mask.add_line(true, "", _mask_ungrouping, "", + _("Ungroup groups created when setting clip/mask")); + + this->AddPage(_page_mask, _("Clippaths and masks"), iter_behavior, PREFS_PAGE_BEHAVIOR_MASKS); + + + _page_markers.add_group_header( _("Stroke Style Markers")); + _page_markers.add_line( true, "", _markers_color_stock, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_custom, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_update, "", + _("Update marker color when object color changes")); + + this->AddPage(_page_markers, _("Markers"), iter_behavior, PREFS_PAGE_BEHAVIOR_MARKERS); + + + _page_cleanup.add_group_header( _("Document cleanup")); + _cleanup_swatches.init ( _("Remove unused swatches when doing a document cleanup"), "/options/cleanupswatches/value", false); // text label + _page_cleanup.add_line( true, "", _cleanup_swatches, "", + _("Remove unused swatches when doing a document cleanup")); // tooltip + this->AddPage(_page_cleanup, _("Cleanup"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLEANUP); +} + +void InkscapePreferences::initPageRendering() +{ + + /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded + _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 8.0, 1.0, 2.0, 4.0, true, false); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), + _("Configure number of processors/threads to use when rendering filters"), false); + + // rendering cache + _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false); + _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false); + + // rendering tile multiplier + _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false); + _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", + _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); + + // rendering xray radius + _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false); + _page_rendering.add_line(false, _("Rendering XRay radius:"), _rendering_xray_radius, "", + _("XRay mode radius preview"), false); + + { + // if these GTK constants ever change, consider adding a compatibility shim to SPCanvas::addIdle() + static_assert(G_PRIORITY_HIGH_IDLE == 100, "G_PRIORITY_HIGH_IDLE must be 100 to match preferences.xml"); + static_assert(G_PRIORITY_DEFAULT_IDLE == 200, "G_PRIORITY_DEFAULT_IDLE must be 200 to match preferences.xml"); + + Glib::ustring redrawPriorityLabels[] = {_("Responsive"), _("Conservative")}; + int redrawPriorityValues[] = {G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE}; + + // redraw priority + _rendering_redraw_priority.init("/options/redrawpriority/value", redrawPriorityLabels, redrawPriorityValues, G_N_ELEMENTS(redrawPriorityLabels), 0); + _page_rendering.add_line(false, _("Redraw while editing:"), _rendering_redraw_priority, "", + _("Set how quickly the canvas display is updated while editing objects"), false); + } + + /* blur quality */ + _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", + BLUR_QUALITY_BEST, false, nullptr); + _blur_quality_better.init ( _("Better quality (slower)"), "/options/blurquality/value", + BLUR_QUALITY_BETTER, false, &_blur_quality_best); + _blur_quality_normal.init ( _("Average quality"), "/options/blurquality/value", + BLUR_QUALITY_NORMAL, true, &_blur_quality_best); + _blur_quality_worse.init ( _("Lower quality (faster)"), "/options/blurquality/value", + BLUR_QUALITY_WORSE, false, &_blur_quality_best); + _blur_quality_worst.init ( _("Lowest quality (fastest)"), "/options/blurquality/value", + BLUR_QUALITY_WORST, false, &_blur_quality_best); + + _page_rendering.add_group_header( _("Gaussian blur quality for display")); + _page_rendering.add_line( true, "", _blur_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _blur_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _blur_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _blur_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _blur_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + + /* filter quality */ + _filter_quality_best.init ( _("Best quality (slowest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BEST, false, nullptr); + _filter_quality_better.init ( _("Better quality (slower)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BETTER, false, &_filter_quality_best); + _filter_quality_normal.init ( _("Average quality"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_NORMAL, true, &_filter_quality_best); + _filter_quality_worse.init ( _("Lower quality (faster)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORSE, false, &_filter_quality_best); + _filter_quality_worst.init ( _("Lowest quality (fastest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORST, false, &_filter_quality_best); + + _page_rendering.add_group_header( _("Filter effects quality for display")); + _page_rendering.add_line( true, "", _filter_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _filter_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _filter_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _filter_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _filter_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + + this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); +} + +void InkscapePreferences::initPageBitmaps() +{ + /* Note: /options/bitmapoversample removed with Cairo renderer */ + _page_bitmaps.add_group_header( _("Edit")); + _misc_bitmap_autoreload.init(_("Automatically reload images"), "/options/bitmapautoreload/value", true); + _page_bitmaps.add_line( false, "", _misc_bitmap_autoreload, "", + _("Automatically reload linked images when file is changed on disk")); + _misc_bitmap_editor.init("/options/bitmapeditor/value", true); + _page_bitmaps.add_line( false, _("_Bitmap editor:"), _misc_bitmap_editor, "", "", true); + _misc_svg_editor.init("/options/svgeditor/value", true); + _page_bitmaps.add_line( false, _("_SVG editor:"), _misc_svg_editor, "", "", true); + + _page_bitmaps.add_group_header( _("Export")); + _importexport_export_res.init("/dialogs/export/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default export _resolution:"), _importexport_export_res, _("dpi"), + _("Default image resolution (in dots per inch) in the Export dialog"), false); + _page_bitmaps.add_group_header( _("Create")); + _bitmap_copy_res.init("/options/createbitmap/resolution", 1.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Resolution for Create Bitmap _Copy:"), _bitmap_copy_res, _("dpi"), + _("Resolution used by the Create Bitmap Copy command"), false); + + _page_bitmaps.add_group_header( _("Import")); + _bitmap_ask.init(_("Ask about linking and scaling when importing bitmap images"), "/dialogs/import/ask", true); + _page_bitmaps.add_line( true, "", _bitmap_ask, "", + _("Pop-up linking and scaling dialog when importing bitmap image.")); + _svg_ask.init(_("Ask about linking and scaling when importing SVG images"), "/dialogs/import/ask_svg", true); + _page_bitmaps.add_line( true, "", _svg_ask, "", + _("Pop-up linking and scaling dialog when importing SVG image.")); + + { + Glib::ustring labels[] = {_("Embed"), _("Link")}; + Glib::ustring values[] = {"embed", "link"}; + _bitmap_link.init("/dialogs/import/link", labels, values, G_N_ELEMENTS(values), "link"); + _page_bitmaps.add_line( false, _("Bitmap import/open mode:"), _bitmap_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("Include"), _("Embed"), _("Link")}; + Glib::ustring values[] = {"include", "embed", "link"}; + _svg_link.init("/dialogs/import/import_mode_svg", labels, values, G_N_ELEMENTS(values), "include"); + _page_bitmaps.add_line( false, _("SVG import mode:"), _svg_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("None (auto)"), _("Smooth (optimizeQuality)"), _("Blocky (optimizeSpeed)") }; + Glib::ustring values[] = {"auto", "optimizeQuality", "optimizeSpeed"}; + _bitmap_scale.init("/dialogs/import/scale", labels, values, G_N_ELEMENTS(values), "scale"); + _page_bitmaps.add_line( false, _("Image scale (image-rendering):"), _bitmap_scale, "", "", false); + } + + /* Note: /dialogs/import/quality removed use of in r12542 */ + _importexport_import_res.init("/dialogs/import/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default _import resolution:"), _importexport_import_res, _("dpi"), + _("Default import resolution (in dots per inch) for bitmap and SVG import"), false); + _importexport_import_res_override.init(_("Override file resolution"), "/dialogs/import/forcexdpi", false); + _page_bitmaps.add_line( false, "", _importexport_import_res_override, "", + _("Use default bitmap resolution in favor of information from file")); + + _page_bitmaps.add_group_header( _("Render")); + // rendering outlines for pixmap image tags + _rendering_image_outline.init( _("Images in Outline Mode"), "/options/rendering/imageinoutlinemode", false); + _page_bitmaps.add_line(false, "", _rendering_image_outline, "", _("When active will render images while in outline mode instead of a red box with an x. This is useful for manual tracing.")); + + this->AddPage(_page_bitmaps, _("Imported Images"), PREFS_PAGE_BITMAPS); +} + +void InkscapePreferences::initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui) +{ + std::vector fileNames; + std::vector fileLabels; + + sp_shortcut_get_file_names(&fileLabels, &fileNames); + + _kb_filelist.init( "/options/kbshortcuts/shortcutfile", &fileLabels[0], &fileNames[0], fileLabels.size(), fileNames[0]); + + Glib::ustring tooltip(_("Select a file of predefined shortcuts to use. Any customized shortcuts you create will be added separately to ")); + tooltip += Glib::ustring(IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, "default.xml")); + + _page_keyshortcuts.add_line( false, _("Shortcut file:"), _kb_filelist, "", tooltip.c_str(), false); + + _kb_search.init("/options/kbshortcuts/value", true); + _page_keyshortcuts.add_line( false, _("Search:"), _kb_search, "", "", true); + + _kb_store = Gtk::TreeStore::create( _kb_columns ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); // only sort in onKBListKeyboardShortcuts() + + _kb_filter = Gtk::TreeModelFilter::create(_kb_store); + _kb_filter->set_visible_func (sigc::mem_fun(*this, &InkscapePreferences::onKBSearchFilter)); + + _kb_shortcut_renderer.property_editable() = true; + + _kb_tree.set_model(_kb_filter); + _kb_tree.append_column(_("Name"), _kb_columns.name); + _kb_tree.append_column(_("Shortcut"), _kb_shortcut_renderer); + _kb_tree.append_column(_("Description"), _kb_columns.description); + _kb_tree.append_column(_("ID"), _kb_columns.id); + + _kb_tree.set_expander_column(*_kb_tree.get_column(0)); + + _kb_tree.get_column(0)->set_resizable(true); + _kb_tree.get_column(0)->set_clickable(true); + _kb_tree.get_column(0)->set_fixed_width (200); + + _kb_tree.get_column(1)->set_resizable(true); + _kb_tree.get_column(1)->set_clickable(true); + _kb_tree.get_column(1)->set_fixed_width (150); + //_kb_tree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), _kb_columns.shortcut); + _kb_tree.get_column(1)->set_cell_data_func(_kb_shortcut_renderer, sigc::ptr_fun(InkscapePreferences::onKBShortcutRenderer)); + + _kb_tree.get_column(2)->set_resizable(true); + _kb_tree.get_column(2)->set_clickable(true); + + _kb_tree.get_column(3)->set_resizable(true); + _kb_tree.get_column(3)->set_clickable(true); + + _kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeEdited) ); + _kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeCleared) ); + + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + scroller->add(_kb_tree); + + int row = 3; + + scroller->set_hexpand(); + scroller->set_vexpand(); + _page_keyshortcuts.attach(*scroller, 0, row, 2, 1); + + row++; + + auto box_buttons = Gtk::manage(new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + + box_buttons->set_hexpand(); + _page_keyshortcuts.attach(*box_buttons, 0, row, 3, 1); + + auto kb_reset = Gtk::manage(new Gtk::Button(_("Reset"))); + kb_reset->set_use_underline(); + kb_reset->set_tooltip_text(_("Remove all your customized keyboard shortcuts, and revert to the shortcuts in the shortcut file listed above")); + box_buttons->pack_start(*kb_reset, true, true, 6); + box_buttons->set_child_secondary(*kb_reset); + + auto kb_import = Gtk::manage(new Gtk::Button(_("Import ..."))); + kb_import->set_use_underline(); + kb_import->set_tooltip_text(_("Import custom keyboard shortcuts from a file")); + box_buttons->pack_end(*kb_import, true, true, 6); + + auto kb_export = Gtk::manage(new Gtk::Button(_("Export ..."))); + kb_export->set_use_underline(); + kb_export->set_tooltip_text(_("Export custom keyboard shortcuts to a file")); + box_buttons->pack_end(*kb_export, true, true, 6); + + kb_reset->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBReset) ); + kb_import->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBImport) ); + kb_export->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBExport) ); + _kb_search.signal_key_release_event().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBSearchKeyEvent) ); + _kb_filelist.signal_changed().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBList) ); + _page_keyshortcuts.signal_realize().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBRealize) ); + + this->AddPage(_page_keyshortcuts, _("Keyboard Shortcuts"), iter_ui, PREFS_PAGE_UI_KEYBOARD_SHORTCUTS); + + _kb_shortcuts_loaded = false; + Gtk::TreeStore::iterator iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = "Loading ..."; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + +} + +void InkscapePreferences::onKBList() +{ + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBReset() +{ + sp_shortcuts_delete_all_from_file(); + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBImport() +{ + if (sp_shortcut_file_import()) { + onKBListKeyboardShortcuts(); + } +} + +void InkscapePreferences::onKBExport() +{ + sp_shortcut_file_export(); +} + +bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/) +{ + _kb_filter->refilter(); + return FALSE; +} + +void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + Glib::ustring id = (*iter)[_kb_columns.id]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + // Remove current shortcut from file + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + + sp_shortcut_init(); + onKBListKeyboardShortcuts(); + +} + +void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + + Glib::ustring id = (*iter)[_kb_columns.id]; + Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + Inkscape::Verb *const verb = Inkscape::Verb::getbyid(id.c_str()); + if (!verb) { + return; + } + + unsigned int const new_shortcut_id = sp_shortcut_get_from_gdk_event(accel_key, accel_mods, hardware_keycode); + if (new_shortcut_id && (new_shortcut_id != current_shortcut_id)) { + // check if there is currently a verb assigned to this shortcut; if yes ask if the shortcut should be reassigned + Inkscape::Verb *current_verb = sp_shortcut_get_verb(new_shortcut_id); + if (current_verb) { + Glib::ustring verb_name = _(current_verb->get_name()); + Glib::ustring::size_type pos = 0; + while ((pos = verb_name.find('_', pos)) != verb_name.npos) { // strip mnemonics + verb_name.erase(pos, 1); + } + Glib::ustring message = Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""), + sp_shortcut_get_label(new_shortcut_id), verb_name); + Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, true); + dialog.set_title(_("Reassign shortcut?")); + dialog.set_secondary_text(_("Are you sure you want to reassign this shortcut?")); + dialog.set_transient_for(*dynamic_cast(get_toplevel())); + int response = dialog.run(); + if (response != Gtk::RESPONSE_YES) { + return; + } + } + + // Delete current shortcut if it existed + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + // Delete any references to the new shortcut + sp_shortcut_delete_from_file(id.c_str(), new_shortcut_id); + // Add the new shortcut + sp_shortcut_add_to_file(id.c_str(), new_shortcut_id); + + sp_shortcut_init(); + onKBListKeyboardShortcuts(); + } +} + +bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter) +{ + Glib::ustring search = _kb_search.get_text().lowercase(); + if (search.empty()) { + return TRUE; + } + + Glib::ustring name = (*iter)[_kb_columns.name]; + Glib::ustring desc = (*iter)[_kb_columns.description]; + Glib::ustring shortcut = (*iter)[_kb_columns.shortcut]; + Glib::ustring id = (*iter)[_kb_columns.id]; + + if (id.empty()) { + return TRUE; // Keep all group nodes visible + } + + return (name.lowercase().find(search) != name.npos + || shortcut.lowercase().find(search) != name.npos + || desc.lowercase().find(search) != name.npos + || id.lowercase().find(search) != name.npos); +} + +void InkscapePreferences::onKBRealize() +{ + if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) { + _kb_shortcuts_loaded = true; + onKBListKeyboardShortcuts(); + } +} + +InkscapePreferences::ModelColumns &InkscapePreferences::onKBGetCols() +{ + static InkscapePreferences::ModelColumns cols; + return cols; +} + +void InkscapePreferences::onKBShortcutRenderer(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) { + + Glib::ustring shortcut = (*iter)[onKBGetCols().shortcut]; + unsigned int user_set = (*iter)[onKBGetCols().user_set]; + Gtk::CellRendererAccel *accel = dynamic_cast(renderer); + if (user_set) { + accel->property_markup() = Glib::ustring(" " + shortcut + " ").c_str(); + } else { + accel->property_markup() = Glib::ustring(" " + shortcut + " ").c_str(); + } +} + +void InkscapePreferences::onKBListKeyboardShortcuts() +{ + // Save the current selection + Gtk::TreeStore::iterator iter = _kb_tree.get_selection()->get_selected(); + Glib::ustring selected_id = ""; + if (iter) { + selected_id = (*iter)[_kb_columns.id]; + } + + _kb_store->clear(); + + std::vectorverbs = Inkscape::Verb::getList(); + + for (auto verb : verbs) { + + if (!verb) { + continue; + } + if (!verb->get_name()){ + continue; + } + + Gtk::TreeStore::Path path; + if (_kb_store->iter_is_valid(_kb_store->get_iter("0"))) { + path = _kb_store->get_path(_kb_store->get_iter("0")); + } + + // Find this group in the tree + Glib::ustring group = verb->get_group() ? _(verb->get_group()) : _("Misc"); + Glib::ustring verb_id = verb->get_id(); + if (verb_id .compare(0,26,"org.inkscape.effect.filter") == 0) { + group = _("Filters"); + } + Gtk::TreeStore::iterator iter_group; + bool found = false; + while (path) { + iter_group = _kb_store->get_iter(path); + if (!_kb_store->iter_is_valid(iter_group)) { + break; + } + Glib::ustring name = (*iter_group)[_kb_columns.name]; + if ((*iter_group)[_kb_columns.name] == group) { + found = true; + break; + } + path.next(); + } + + if (!found) { + // Add the group if not there + iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = group; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + } + + // Remove the key accelerators from the verb name + Glib::ustring name = _(verb->get_name()); + std::string::size_type k = 0; + while((k=name.find('_',k))!=name.npos) { + name.erase(k, 1); + } + + // Get the shortcut label + unsigned int shortcut_id = sp_shortcut_get_primary(verb); + Glib::ustring shortcut_label = ""; + if (shortcut_id != GDK_KEY_VoidSymbol) { + gchar* str = sp_shortcut_get_label(shortcut_id); + if (str) { + shortcut_label = Glib::Markup::escape_text(str); + g_free(str); + str = nullptr; + } + } + // Add the verb to the group + Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children()); + (*row)[_kb_columns.name] = name; + (*row)[_kb_columns.shortcut] = shortcut_label; + (*row)[_kb_columns.description] = verb->get_short_tip() ? _(verb->get_short_tip()) : ""; + (*row)[_kb_columns.shortcutid] = shortcut_id; + (*row)[_kb_columns.id] = verb->get_id(); + (*row)[_kb_columns.user_set] = sp_shortcut_is_user_set(verb); + + if (selected_id == verb->get_id()) { + Gtk::TreeStore::Path sel_path = _kb_filter->convert_child_path_to_path(_kb_store->get_path(row)); + _kb_tree.expand_to_path(sel_path); + _kb_tree.get_selection()->select(sel_path); + } + } + + // re-order once after updating (then disable ordering again to increase performance) + _kb_store->set_sort_column (_kb_columns.id, Gtk::SORT_ASCENDING ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); + + if (selected_id.empty()) { + _kb_tree.expand_to_path(_kb_store->get_path(_kb_store->get_iter("0:1"))); + } + +} + +void InkscapePreferences::initPageSpellcheck() +{ +#if HAVE_ASPELL + + std::vector languages; + std::vector langValues; + + languages.emplace_back(C_("Spellchecker language", "None")); + langValues.emplace_back(""); + + for (auto const &lang : SpellCheck::get_available_langs()) { + languages.emplace_back(lang); + langValues.emplace_back(lang); + } + + _spell_language.init( "/dialogs/spellcheck/lang", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Language:"), _spell_language, "", + _("Set the main spell check language"), false); + + _spell_language2.init( "/dialogs/spellcheck/lang2", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Second language:"), _spell_language2, "", + _("Set the second spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _spell_language3.init( "/dialogs/spellcheck/lang3", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Third language:"), _spell_language3, "", + _("Set the third spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _spell_ignorenumbers.init( _("Ignore words with digits"), "/dialogs/spellcheck/ignorenumbers", true); + _page_spellcheck.add_line( false, "", _spell_ignorenumbers, "", + _("Ignore words containing digits, such as \"R2D2\""), true); + + _spell_ignoreallcaps.init( _("Ignore words in ALL CAPITALS"), "/dialogs/spellcheck/ignoreallcaps", false); + _page_spellcheck.add_line( false, "", _spell_ignoreallcaps, "", + _("Ignore words in all capitals, such as \"IUPAC\""), true); + + this->AddPage(_page_spellcheck, _("Spellcheck"), PREFS_PAGE_SPELLCHECK); +#endif +} + +static void appendList( Glib::ustring& tmp, const gchar* const*listing ) +{ + for (const gchar* const* ptr = listing; *ptr; ptr++) { + tmp += *ptr; + tmp += "\n"; + } +} + +void InkscapePreferences::initPageSystem() +{ + _misc_latency_skew.init("/debug/latency/skew", 0.5, 2.0, 0.01, 0.10, 1.0, false, false); + _page_system.add_line( false, _("Latency _skew:"), _misc_latency_skew, _("(requires restart)"), + _("Factor by which the event clock is skewed from the actual time (0.9766 on some systems)"), false); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _misc_namedicon_delay.init( _("Pre-render named icons"), "/options/iconrender/named_nodelay", false); + _page_system.add_line( false, "", _misc_namedicon_delay, "", + _("When on, named icons will be rendered before displaying the ui. This is for working around bugs in GTK+ named icon notification"), true); + + _page_system.add_group_header( _("System info")); + + _sys_user_prefs.set_text(prefs->getPrefsFilename()); + _sys_user_prefs.set_editable(false); + Gtk::Button* reset_prefs = Gtk::manage(new Gtk::Button(_("Reset Preferences"))); + reset_prefs->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_prefs_clicked)); + + _page_system.add_line(true, _("User preferences: "), _sys_user_prefs, "", + _("Location of the user’s preferences file"), true, reset_prefs); + + _sys_user_config.init((char const *)Inkscape::IO::Resource::profile_path(""), _("Open preferences folder")); + _page_system.add_line(true, _("User config: "), _sys_user_config, "", _("Location of users configuration"), true); + + _sys_user_extension_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::EXTENSIONS, ""), + _("Open extensions folder")); + _page_system.add_line(true, _("User extensions: "), _sys_user_extension_dir, "", + _("Location of the user’s extensions"), true); + + _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("Open themes folder")); + _page_system.add_line(true, _("User themes: "), _sys_user_themes_dir, "", _("Location of the user’s themes"), true); + + _sys_user_icons_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_system.add_line(true, _("User icons: "), _sys_user_icons_dir, "", _("Location of the user’s icons"), true); + + _sys_user_templates_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::TEMPLATES, ""), + _("Open templates folder")); + _page_system.add_line(true, _("User templates: "), _sys_user_templates_dir, "", + _("Location of the user’s templates"), true); + + _sys_user_symbols_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::SYMBOLS, ""), + _("Open symbols folder")); + + _page_system.add_line(true, _("User symbols: "), _sys_user_symbols_dir, "", _("Location of the user’s symbols"), + true); + + _sys_user_paint_servers_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PAINT, ""), + _("Open paint servers folder")); + + _page_system.add_line(true, _("User paint servers: "), _sys_user_paint_servers_dir, "", + _("Location of the user’s paint servers"), true); + + _sys_user_palettes_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PALETTES, ""), + _("Open palettes folder")); + _page_system.add_line(true, _("User palettes: "), _sys_user_palettes_dir, "", _("Location of the user’s palettes"), + true); + + _sys_user_keys_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, ""), + _("Open keyboard shortcuts folder")); + _page_system.add_line(true, _("User keys: "), _sys_user_keys_dir, "", + _("Location of the user’s keyboard mapping files"), true); + + _sys_user_ui_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::UIS, ""), + _("Open user interface folder")); + _page_system.add_line(true, _("User UI: "), _sys_user_ui_dir, "", + _("Location of the user’s user interface description files"), true); + + _sys_user_cache.set_text(g_get_user_cache_dir()); + _sys_user_cache.set_editable(false); + _page_system.add_line(true, _("User cache: "), _sys_user_cache, "", _("Location of user’s cache"), true); + + Glib::ustring tmp_dir = prefs->getString("/options/autosave/path"); + if (tmp_dir.empty()) { + tmp_dir = Glib::build_filename(Glib::get_user_cache_dir(), "inkscape"); + } + _sys_tmp_files.set_text(tmp_dir); + _sys_tmp_files.set_editable(false); + _page_system.add_line(true, _("Temporary files: "), _sys_tmp_files, "", _("Location of the temporary files used for autosave"), true); + + _sys_data.set_text( INKSCAPE_DATADIR_REAL ); + _sys_data.set_editable(false); + _page_system.add_line(true, _("Inkscape data: "), _sys_data, "", _("Location of Inkscape data"), true); + + _sys_extension_dir.set_text(INKSCAPE_EXTENSIONDIR); + _sys_extension_dir.set_editable(false); + _page_system.add_line(true, _("Inkscape extensions: "), _sys_extension_dir, "", _("Location of the Inkscape extensions"), true); + + Glib::ustring tmp; + appendList( tmp, g_get_system_data_dirs() ); + _sys_systemdata.get_buffer()->insert(_sys_systemdata.get_buffer()->end(), tmp); + _sys_systemdata.set_editable(false); + _sys_systemdata_scroll.add(_sys_systemdata); + _sys_systemdata_scroll.set_size_request(100, 80); + _sys_systemdata_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_systemdata_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("System data: "), _sys_systemdata_scroll, "", _("Locations of system data"), true); + + tmp = ""; + gchar** paths = nullptr; + gint count = 0; + gtk_icon_theme_get_search_path(gtk_icon_theme_get_default(), &paths, &count); + appendList( tmp, paths ); + g_strfreev(paths); + _sys_icon.get_buffer()->insert(_sys_icon.get_buffer()->end(), tmp); + _sys_icon.set_editable(false); + _sys_icon_scroll.add(_sys_icon); + _sys_icon_scroll.set_size_request(100, 80); + _sys_icon_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_icon_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("Icon theme: "), _sys_icon_scroll, "", _("Locations of icon themes"), true); + + this->AddPage(_page_system, _("System"), PREFS_PAGE_SYSTEM); +} + +bool InkscapePreferences::GetSizeRequest(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + DialogPage* page = row[_page_list_columns._col_page]; + _page_frame.add(*page); + this->show_all_children(); + Gtk::Requisition sreq_minimum; + Gtk::Requisition sreq_natural; + _getContents()->get_preferred_size(sreq_minimum, sreq_natural); + _minimum_width = std::max(_minimum_width, sreq_minimum.width); + _minimum_height = std::max(_minimum_height, sreq_minimum.height); + _natural_width = std::max(_natural_width, sreq_natural.width); + _natural_height = std::max(_natural_height, sreq_natural.height); + _page_frame.remove(); + return false; +} + +bool InkscapePreferences::PresentPage(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int desired_page = prefs->getInt("/dialogs/preferences/page", 0); + _init = false; + if (desired_page == row[_page_list_columns._col_id]) + { + if (desired_page >= PREFS_PAGE_TOOLS && desired_page <= PREFS_PAGE_TOOLS_CONNECTOR) + _page_list.expand_row(_path_tools, false); + if (desired_page >= PREFS_PAGE_TOOLS_SHAPES && desired_page <= PREFS_PAGE_TOOLS_SHAPES_SPIRAL) + _page_list.expand_row(_path_shapes, false); + if (desired_page >= PREFS_PAGE_UI && desired_page <= PREFS_PAGE_UI_KEYBOARD_SHORTCUTS) + _page_list.expand_row(_path_ui, false); + if (desired_page >= PREFS_PAGE_BEHAVIOR && desired_page <= PREFS_PAGE_BEHAVIOR_MASKS) + _page_list.expand_row(_path_behavior, false); + if (desired_page >= PREFS_PAGE_IO && desired_page <= PREFS_PAGE_IO_OPENCLIPART) + _page_list.expand_row(_path_io, false); + _page_list.get_selection()->select(iter); + if (desired_page == PREFS_PAGE_UI_THEME) + symbolicThemeCheck(); + return true; + } + return false; +} + +void InkscapePreferences::on_reset_open_recent_clicked() +{ + Glib::RefPtr manager = Gtk::RecentManager::get_default(); + std::vector< Glib::RefPtr< Gtk::RecentInfo > > recent_list = manager->get_items(); + + // Remove only elements that were added by Inkscape + // TODO: This should likely preserve items that were also accessed by other apps. + // However there does not seem to be straightforward way to delete only an application from an item. + for (auto e : recent_list) { + if (e->has_application(g_get_prgname()) + || e->has_application("org.inkscape.Inkscape") + || e->has_application("inkscape") +#ifdef _WIN32 + || e->has_application("inkscape.exe") +#endif + ) { + manager->remove_item(e->get_uri()); + } + } +} + +void InkscapePreferences::on_reset_prefs_clicked() +{ + Inkscape::Preferences::get()->reset(); +} + +void InkscapePreferences::on_pagelist_selection_changed() +{ + // show new selection + Glib::RefPtr selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if(iter) + { + if (_current_page) + _page_frame.remove(); + Gtk::TreeModel::Row row = *iter; + _current_page = row[_page_list_columns._col_page]; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!_init) { + prefs->setInt("/dialogs/preferences/page", row[_page_list_columns._col_id]); + } + Glib::ustring col_name_escaped = Glib::Markup::escape_text( row[_page_list_columns._col_name] ); + _page_title.set_markup("" + col_name_escaped + ""); + _page_frame.add(*_current_page); + _current_page->show(); + while (Gtk::Main::events_pending()) + { + Gtk::Main::iteration(); + } + this->show_all_children(); + if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) { + symbolicThemeCheck(); + } + } +} + +void InkscapePreferences::_presentPages() +{ + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::PresentPage)); +} + +} // 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 : diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h new file mode 100644 index 0000000..af7aa26 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.h @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape Preferences dialog + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen + * Bruno Dilly + * + * Copyright (C) 2004-2013 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H + +#include +#include +#include "ui/widget/preferences-widget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui/widget/panel.h" + +// UPDATE THIS IF YOU'RE ADDING PREFS PAGES. +// Otherwise the commands that open the dialog with the new page will fail. + +enum { + PREFS_PAGE_TOOLS, + PREFS_PAGE_TOOLS_SELECTOR, + PREFS_PAGE_TOOLS_NODE, + PREFS_PAGE_TOOLS_TWEAK, + PREFS_PAGE_TOOLS_ZOOM, + PREFS_PAGE_TOOLS_MEASURE, + PREFS_PAGE_TOOLS_SHAPES, + PREFS_PAGE_TOOLS_SHAPES_RECT, + PREFS_PAGE_TOOLS_SHAPES_3DBOX, + PREFS_PAGE_TOOLS_SHAPES_ELLIPSE, + PREFS_PAGE_TOOLS_SHAPES_STAR, + PREFS_PAGE_TOOLS_SHAPES_SPIRAL, + PREFS_PAGE_TOOLS_PENCIL, + PREFS_PAGE_TOOLS_PEN, + PREFS_PAGE_TOOLS_CALLIGRAPHY, + PREFS_PAGE_TOOLS_TEXT, + PREFS_PAGE_TOOLS_SPRAY, + PREFS_PAGE_TOOLS_ERASER, + PREFS_PAGE_TOOLS_PAINTBUCKET, + PREFS_PAGE_TOOLS_GRADIENT, + PREFS_PAGE_TOOLS_DROPPER, + PREFS_PAGE_TOOLS_CONNECTOR, + PREFS_PAGE_TOOLS_LPETOOL, + PREFS_PAGE_UI, + PREFS_PAGE_UI_THEME, + PREFS_PAGE_UI_WINDOWS, + PREFS_PAGE_UI_GRIDS, + PREFS_PAGE_UI_KEYBOARD_SHORTCUTS, + PREFS_PAGE_BEHAVIOR, + PREFS_PAGE_BEHAVIOR_SELECTING, + PREFS_PAGE_BEHAVIOR_TRANSFORMS, + PREFS_PAGE_BEHAVIOR_DASHES, + PREFS_PAGE_BEHAVIOR_SCROLLING, + PREFS_PAGE_BEHAVIOR_SNAPPING, + PREFS_PAGE_BEHAVIOR_STEPS, + PREFS_PAGE_BEHAVIOR_CLONES, + PREFS_PAGE_BEHAVIOR_MASKS, + PREFS_PAGE_BEHAVIOR_MARKERS, + PREFS_PAGE_BEHAVIOR_CLEANUP, + PREFS_PAGE_IO, + PREFS_PAGE_IO_MOUSE, + PREFS_PAGE_IO_SVGOUTPUT, + PREFS_PAGE_IO_SVGEXPORT, + PREFS_PAGE_IO_CMS, + PREFS_PAGE_IO_AUTOSAVE, + PREFS_PAGE_IO_OPENCLIPART, + PREFS_PAGE_SYSTEM, + PREFS_PAGE_BITMAPS, + PREFS_PAGE_RENDERING, + PREFS_PAGE_SPELLCHECK +}; + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InkscapePreferences : public UI::Widget::Panel { +public: + ~InkscapePreferences() override; + + static InkscapePreferences &getInstance() { return *new InkscapePreferences(); } + +protected: + Gtk::Frame _page_frame; + Gtk::Label _page_title; + Gtk::TreeView _page_list; + Glib::RefPtr _page_list_model; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PageListModelColumns() + { Gtk::TreeModelColumnRecord::add(_col_name); Gtk::TreeModelColumnRecord::add(_col_page); Gtk::TreeModelColumnRecord::add(_col_id); } + Gtk::TreeModelColumn _col_name; + Gtk::TreeModelColumn _col_id; + Gtk::TreeModelColumn _col_page; + }; + PageListModelColumns _page_list_columns; + + Gtk::TreeModel::Path _path_tools; + Gtk::TreeModel::Path _path_shapes; + Gtk::TreeModel::Path _path_ui; + Gtk::TreeModel::Path _path_behavior; + Gtk::TreeModel::Path _path_io; + + UI::Widget::DialogPage _page_tools; + UI::Widget::DialogPage _page_selector; + UI::Widget::DialogPage _page_node; + UI::Widget::DialogPage _page_tweak; + UI::Widget::DialogPage _page_spray; + UI::Widget::DialogPage _page_zoom; + UI::Widget::DialogPage _page_measure; + UI::Widget::DialogPage _page_shapes; + UI::Widget::DialogPage _page_pencil; + UI::Widget::DialogPage _page_pen; + UI::Widget::DialogPage _page_calligraphy; + UI::Widget::DialogPage _page_text; + UI::Widget::DialogPage _page_gradient; + UI::Widget::DialogPage _page_connector; + UI::Widget::DialogPage _page_dropper; + UI::Widget::DialogPage _page_lpetool; + + UI::Widget::DialogPage _page_rectangle; + UI::Widget::DialogPage _page_3dbox; + UI::Widget::DialogPage _page_ellipse; + UI::Widget::DialogPage _page_star; + UI::Widget::DialogPage _page_spiral; + UI::Widget::DialogPage _page_paintbucket; + UI::Widget::DialogPage _page_eraser; + + UI::Widget::DialogPage _page_ui; + UI::Widget::DialogPage _page_theme; + UI::Widget::DialogPage _page_windows; + UI::Widget::DialogPage _page_grids; + + UI::Widget::DialogPage _page_behavior; + UI::Widget::DialogPage _page_select; + UI::Widget::DialogPage _page_transforms; + UI::Widget::DialogPage _page_dashes; + UI::Widget::DialogPage _page_scrolling; + UI::Widget::DialogPage _page_snapping; + UI::Widget::DialogPage _page_steps; + UI::Widget::DialogPage _page_clones; + UI::Widget::DialogPage _page_mask; + UI::Widget::DialogPage _page_markers; + UI::Widget::DialogPage _page_cleanup; + + UI::Widget::DialogPage _page_io; + UI::Widget::DialogPage _page_mouse; + UI::Widget::DialogPage _page_svgoutput; + UI::Widget::DialogPage _page_svgexport; + UI::Widget::DialogPage _page_cms; + UI::Widget::DialogPage _page_autosave; + + UI::Widget::DialogPage _page_rendering; + UI::Widget::DialogPage _page_system; + UI::Widget::DialogPage _page_bitmaps; + UI::Widget::DialogPage _page_spellcheck; + + UI::Widget::DialogPage _page_keyshortcuts; + + UI::Widget::PrefSpinButton _mouse_sens; + UI::Widget::PrefSpinButton _mouse_thres; + UI::Widget::PrefSlider _mouse_grabsize; + UI::Widget::PrefCheckButton _mouse_use_ext_input; + UI::Widget::PrefCheckButton _mouse_switch_on_ext_input; + + UI::Widget::PrefSpinButton _scroll_wheel; + UI::Widget::PrefSpinButton _scroll_arrow_px; + UI::Widget::PrefSpinButton _scroll_arrow_acc; + UI::Widget::PrefSpinButton _scroll_auto_speed; + UI::Widget::PrefSpinButton _scroll_auto_thres; + UI::Widget::PrefCheckButton _scroll_space; + UI::Widget::PrefCheckButton _wheel_zoom; + + Gtk::Scale *_slider_snapping_delay; + + UI::Widget::PrefCheckButton _snap_default; + UI::Widget::PrefCheckButton _snap_indicator; + UI::Widget::PrefCheckButton _snap_closest_only; + UI::Widget::PrefCheckButton _snap_mouse_pointer; + + UI::Widget::PrefCombo _steps_rot_snap; + UI::Widget::PrefCheckButton _steps_rot_relative; + UI::Widget::PrefCheckButton _steps_compass; + UI::Widget::PrefSpinUnit _steps_arrow; + UI::Widget::PrefSpinUnit _steps_scale; + UI::Widget::PrefSpinUnit _steps_inset; + UI::Widget::PrefSpinButton _steps_zoom; + UI::Widget::PrefCheckButton _middle_mouse_zoom; + UI::Widget::PrefSpinButton _steps_rotate; + + UI::Widget::PrefRadioButton _t_sel_trans_obj; + UI::Widget::PrefRadioButton _t_sel_trans_outl; + UI::Widget::PrefRadioButton _t_sel_cue_none; + UI::Widget::PrefRadioButton _t_sel_cue_mark; + UI::Widget::PrefRadioButton _t_sel_cue_box; + UI::Widget::PrefRadioButton _t_bbox_visual; + UI::Widget::PrefRadioButton _t_bbox_geometric; + + UI::Widget::PrefCheckButton _t_cvg_keep_objects; + UI::Widget::PrefCheckButton _t_cvg_convert_whole_groups; + UI::Widget::PrefCheckButton _t_node_show_outline; + UI::Widget::PrefCheckButton _t_node_live_outline; + UI::Widget::PrefCheckButton _t_node_live_objects; + UI::Widget::PrefCheckButton _t_node_pathflash_enabled; + UI::Widget::PrefCheckButton _t_node_pathflash_selected; + UI::Widget::PrefSpinButton _t_node_pathflash_timeout; + UI::Widget::PrefCheckButton _t_node_show_path_direction; + UI::Widget::PrefCheckButton _t_node_single_node_transform_handles; + UI::Widget::PrefCheckButton _t_node_delete_preserves_shape; + UI::Widget::PrefColorPicker _t_node_pathoutline_color; + + UI::Widget::PrefCombo _gtk_theme; + UI::Widget::PrefOpenFolder _sys_user_themes_dir_copy; + UI::Widget::PrefOpenFolder _sys_user_icons_dir_copy; + UI::Widget::PrefCombo _icon_theme; + UI::Widget::PrefCheckButton _dark_theme; + UI::Widget::PrefCheckButton _symbolic_icons; + UI::Widget::PrefCheckButton _symbolic_base_colors; + UI::Widget::PrefColorPicker _symbolic_base_color; + UI::Widget::PrefColorPicker _symbolic_warning_color; + UI::Widget::PrefColorPicker _symbolic_error_color; + UI::Widget::PrefColorPicker _symbolic_success_color; + /* Gtk::Image *_complementary_colors; */ + UI::Widget::PrefCombo _misc_small_toolbar; + UI::Widget::PrefCombo _misc_small_secondary; + UI::Widget::PrefCombo _misc_small_tools; + UI::Widget::PrefCombo _menu_icons; + + Gtk::Button _apply_theme; + + UI::Widget::PrefRadioButton _win_dockable; + UI::Widget::PrefRadioButton _win_floating; + UI::Widget::PrefRadioButton _win_native; + UI::Widget::PrefRadioButton _win_gtk; + UI::Widget::PrefRadioButton _win_save_dialog_pos_on; + UI::Widget::PrefRadioButton _win_save_dialog_pos_off; + UI::Widget::PrefCombo _win_default_size; + UI::Widget::PrefRadioButton _win_ontop_none; + UI::Widget::PrefRadioButton _win_ontop_normal; + UI::Widget::PrefRadioButton _win_ontop_agressive; + UI::Widget::PrefRadioButton _win_save_geom_off; + UI::Widget::PrefRadioButton _win_save_geom; + UI::Widget::PrefRadioButton _win_save_geom_prefs; + UI::Widget::PrefCheckButton _win_hide_task; + UI::Widget::PrefCheckButton _win_save_viewport; + UI::Widget::PrefCheckButton _win_zoom_resize; + + UI::Widget::PrefCheckButton _pencil_average_all_sketches; + + UI::Widget::PrefCheckButton _calligrapy_use_abs_size; + UI::Widget::PrefCheckButton _calligrapy_keep_selected; + + UI::Widget::PrefCheckButton _connector_ignore_text; + + UI::Widget::PrefRadioButton _clone_option_parallel; + UI::Widget::PrefRadioButton _clone_option_stay; + UI::Widget::PrefRadioButton _clone_option_transform; + UI::Widget::PrefRadioButton _clone_option_unlink; + UI::Widget::PrefRadioButton _clone_option_delete; + UI::Widget::PrefCheckButton _clone_relink_on_duplicate; + UI::Widget::PrefCheckButton _clone_to_curves; + + UI::Widget::PrefCheckButton _mask_mask_on_top; + UI::Widget::PrefCheckButton _mask_mask_remove; + UI::Widget::PrefRadioButton _mask_grouping_none; + UI::Widget::PrefRadioButton _mask_grouping_separate; + UI::Widget::PrefRadioButton _mask_grouping_all; + UI::Widget::PrefCheckButton _mask_ungrouping; + + UI::Widget::PrefRadioButton _blur_quality_best; + UI::Widget::PrefRadioButton _blur_quality_better; + UI::Widget::PrefRadioButton _blur_quality_normal; + UI::Widget::PrefRadioButton _blur_quality_worse; + UI::Widget::PrefRadioButton _blur_quality_worst; + UI::Widget::PrefRadioButton _filter_quality_best; + UI::Widget::PrefRadioButton _filter_quality_better; + UI::Widget::PrefRadioButton _filter_quality_normal; + UI::Widget::PrefRadioButton _filter_quality_worse; + UI::Widget::PrefRadioButton _filter_quality_worst; + UI::Widget::PrefCheckButton _show_filters_info_box; + UI::Widget::PrefCombo _dockbar_style; + UI::Widget::PrefCombo _switcher_style; + UI::Widget::PrefCheckButton _rendering_image_outline; + UI::Widget::PrefSpinButton _rendering_cache_size; + UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_xray_radius; + UI::Widget::PrefCombo _rendering_redraw_priority; + UI::Widget::PrefSpinButton _filter_multi_threaded; + + UI::Widget::PrefCheckButton _trans_scale_stroke; + UI::Widget::PrefCheckButton _trans_scale_corner; + UI::Widget::PrefCheckButton _trans_gradient; + UI::Widget::PrefCheckButton _trans_pattern; + UI::Widget::PrefRadioButton _trans_optimized; + UI::Widget::PrefRadioButton _trans_preserved; + + UI::Widget::PrefCheckButton _dash_scale; + + UI::Widget::PrefRadioButton _sel_all; + UI::Widget::PrefRadioButton _sel_current; + UI::Widget::PrefRadioButton _sel_recursive; + UI::Widget::PrefCheckButton _sel_hidden; + UI::Widget::PrefCheckButton _sel_locked; + UI::Widget::PrefCheckButton _sel_layer_deselects; + UI::Widget::PrefCheckButton _sel_cycle; + + UI::Widget::PrefCheckButton _markers_color_stock; + UI::Widget::PrefCheckButton _markers_color_custom; + UI::Widget::PrefCheckButton _markers_color_update; + + UI::Widget::PrefCheckButton _cleanup_swatches; + + UI::Widget::PrefSpinButton _importexport_export_res; + UI::Widget::PrefSpinButton _importexport_import_res; + UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefSlider _snap_delay; + UI::Widget::PrefSlider _snap_weight; + UI::Widget::PrefSlider _snap_persistence; + UI::Widget::PrefCheckButton _font_dialog; + UI::Widget::PrefCombo _font_unit_type; + UI::Widget::PrefCheckButton _font_output_px; + UI::Widget::PrefCheckButton _font_fontsdir_system; + UI::Widget::PrefCheckButton _font_fontsdir_user; + UI::Widget::PrefMultiEntry _font_fontdirs_custom; + + UI::Widget::PrefCheckButton _misc_comment; + UI::Widget::PrefCheckButton _misc_default_metadata; + UI::Widget::PrefCheckButton _misc_forkvectors; + UI::Widget::PrefCheckButton _misc_gradienteditor; + UI::Widget::PrefSpinButton _misc_gradientangle; + UI::Widget::PrefCheckButton _misc_scripts; + UI::Widget::PrefCheckButton _misc_namedicon_delay; + + // System page + // Gtk::Button *_apply_theme; + UI::Widget::PrefSpinButton _misc_latency_skew; + UI::Widget::PrefSpinButton _misc_simpl; + Gtk::Entry _sys_user_prefs; + Gtk::Entry _sys_tmp_files; + Gtk::Entry _sys_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_config; + UI::Widget::PrefOpenFolder _sys_user_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_themes_dir; + UI::Widget::PrefOpenFolder _sys_user_ui_dir; + UI::Widget::PrefOpenFolder _sys_user_icons_dir; + UI::Widget::PrefOpenFolder _sys_user_keys_dir; + UI::Widget::PrefOpenFolder _sys_user_palettes_dir; + UI::Widget::PrefOpenFolder _sys_user_templates_dir; + UI::Widget::PrefOpenFolder _sys_user_symbols_dir; + UI::Widget::PrefOpenFolder _sys_user_paint_servers_dir; + Gtk::Entry _sys_user_cache; + Gtk::Entry _sys_data; + Gtk::TextView _sys_icon; + Gtk::ScrolledWindow _sys_icon_scroll; + Gtk::TextView _sys_systemdata; + Gtk::ScrolledWindow _sys_systemdata_scroll; + + // UI page + UI::Widget::PrefCombo _ui_languages; + UI::Widget::PrefCheckButton _ui_colorsliders_top; + UI::Widget::PrefSpinButton _misc_recent; + UI::Widget::PrefCheckButton _ui_partialdynamic; + UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _ui_yaxisdown; + UI::Widget::PrefCheckButton _ui_rotationlock; + + //Spellcheck + UI::Widget::PrefCombo _spell_language; + UI::Widget::PrefCombo _spell_language2; + UI::Widget::PrefCombo _spell_language3; + UI::Widget::PrefCheckButton _spell_ignorenumbers; + UI::Widget::PrefCheckButton _spell_ignoreallcaps; + + // Bitmaps + UI::Widget::PrefCombo _misc_overs_bitmap; + UI::Widget::PrefEntryFileButtonHBox _misc_bitmap_editor; + UI::Widget::PrefEntryFileButtonHBox _misc_svg_editor; + UI::Widget::PrefCheckButton _misc_bitmap_autoreload; + UI::Widget::PrefSpinButton _bitmap_copy_res; + UI::Widget::PrefCheckButton _bitmap_ask; + UI::Widget::PrefCheckButton _svg_ask; + UI::Widget::PrefCombo _bitmap_link; + UI::Widget::PrefCombo _svg_link; + UI::Widget::PrefCombo _bitmap_scale; + UI::Widget::PrefSpinButton _bitmap_import_quality; + + UI::Widget::PrefEntry _kb_search; + UI::Widget::PrefCombo _kb_filelist; + + UI::Widget::PrefCheckButton _save_use_current_dir; + UI::Widget::PrefCheckButton _save_autosave_enable; + UI::Widget::PrefSpinButton _save_autosave_interval; + UI::Widget::PrefEntry _save_autosave_path; + UI::Widget::PrefSpinButton _save_autosave_max; + + Gtk::ComboBoxText _cms_display_profile; + UI::Widget::PrefCheckButton _cms_from_display; + UI::Widget::PrefCombo _cms_intent; + + UI::Widget::PrefCheckButton _cms_softproof; + UI::Widget::PrefCheckButton _cms_gamutwarn; + Gtk::ColorButton _cms_gamutcolor; + Gtk::ComboBoxText _cms_proof_profile; + UI::Widget::PrefCombo _cms_proof_intent; + UI::Widget::PrefCheckButton _cms_proof_blackpoint; + UI::Widget::PrefCheckButton _cms_proof_preserveblack; + + Gtk::Notebook _grids_notebook; + UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom; + UI::Widget::PrefRadioButton _grids_emphasize_on_zoom; + UI::Widget::DialogPage _grids_xy; + UI::Widget::DialogPage _grids_axonom; + // CanvasXYGrid properties: + UI::Widget::PrefUnit _grids_xy_units; + UI::Widget::PrefSpinButton _grids_xy_origin_x; + UI::Widget::PrefSpinButton _grids_xy_origin_y; + UI::Widget::PrefSpinButton _grids_xy_spacing_x; + UI::Widget::PrefSpinButton _grids_xy_spacing_y; + UI::Widget::PrefColorPicker _grids_xy_color; + UI::Widget::PrefColorPicker _grids_xy_empcolor; + UI::Widget::PrefSpinButton _grids_xy_empspacing; + UI::Widget::PrefCheckButton _grids_xy_dotted; + // CanvasAxonomGrid properties: + UI::Widget::PrefUnit _grids_axonom_units; + UI::Widget::PrefSpinButton _grids_axonom_origin_x; + UI::Widget::PrefSpinButton _grids_axonom_origin_y; + UI::Widget::PrefSpinButton _grids_axonom_spacing_y; + UI::Widget::PrefSpinButton _grids_axonom_angle_x; + UI::Widget::PrefSpinButton _grids_axonom_angle_z; + UI::Widget::PrefColorPicker _grids_axonom_color; + UI::Widget::PrefColorPicker _grids_axonom_empcolor; + UI::Widget::PrefSpinButton _grids_axonom_empspacing; + + // SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_usenamedcolors; + UI::Widget::PrefSpinButton _svgoutput_numericprecision; + UI::Widget::PrefSpinButton _svgoutput_minimumexponent; + UI::Widget::PrefCheckButton _svgoutput_inlineattrs; + UI::Widget::PrefSpinButton _svgoutput_indent; + UI::Widget::PrefCombo _svgoutput_pathformat; + UI::Widget::PrefCheckButton _svgoutput_forcerepeatcommands; + + // Attribute Checking controls for SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_attrwarn; + UI::Widget::PrefCheckButton _svgoutput_attrremove; + UI::Widget::PrefCheckButton _svgoutput_stylepropwarn; + UI::Widget::PrefCheckButton _svgoutput_stylepropremove; + UI::Widget::PrefCheckButton _svgoutput_styledefaultswarn; + UI::Widget::PrefCheckButton _svgoutput_styledefaultsremove; + UI::Widget::PrefCheckButton _svgoutput_check_reading; + UI::Widget::PrefCheckButton _svgoutput_check_editing; + UI::Widget::PrefCheckButton _svgoutput_check_writing; + + // SVG Output export: + UI::Widget::PrefCheckButton _svgexport_insert_text_fallback; + UI::Widget::PrefCheckButton _svgexport_insert_mesh_polyfill; + UI::Widget::PrefCheckButton _svgexport_insert_hatch_polyfill; + UI::Widget::PrefCheckButton _svgexport_remove_marker_auto_start_reverse; + UI::Widget::PrefCheckButton _svgexport_remove_marker_context_paint; + + + /* + * Keyboard shortcut members + */ + class ModelColumns: public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(name); + add(id); + add(shortcut); + add(description); + add(shortcutid); + add(user_set); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn id; + Gtk::TreeModelColumn shortcut; + Gtk::TreeModelColumn description; + Gtk::TreeModelColumn shortcutid; + Gtk::TreeModelColumn user_set; + }; + ModelColumns _kb_columns; + static ModelColumns &onKBGetCols(); + Glib::RefPtr _kb_store; + Gtk::TreeView _kb_tree; + Gtk::CellRendererAccel _kb_shortcut_renderer; + Glib::RefPtr _kb_filter; + gboolean _kb_shortcuts_loaded; + + int _minimum_width; + int _minimum_height; + int _natural_width; + int _natural_height; + bool GetSizeRequest(const Gtk::TreeModel::iterator& iter); + void get_preferred_width_vfunc (int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_width_for_height_vfunc (int height, int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_height_vfunc (int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + void get_preferred_height_for_width_vfunc (int width, int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + int _sb_width; + UI::Widget::DialogPage* _current_page; + + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, int id); + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id); + bool PresentPage(const Gtk::TreeModel::iterator& iter); + + static void AddSelcueCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddGradientCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddConvertGuidesCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddFirstAndLastCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddDotSizeSpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddBaseSimplifySpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddNewObjectsStyle(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, const gchar* banner = nullptr); + + void on_pagelist_selection_changed(); + void on_reset_open_recent_clicked(); + void on_reset_prefs_clicked(); + + void initPageTools(); + void initPageUI(); + void initPageBehavior(); + void initPageIO(); + + void initPageRendering(); + void initPageSpellcheck(); + void initPageBitmaps(); + void initPageSystem(); + void initPageI18n(); // Do we still need it? + void initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui); + + void _presentPages(); + + /* + * Functions for the Keyboard shortcut editor panel + */ + void onKBReset(); + void onKBImport(); + void onKBExport(); + void onKBList(); + void onKBRealize(); + void onKBListKeyboardShortcuts(); + void onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode); + void onKBTreeCleared(const Glib::ustring& path_string); + bool onKBSearchKeyEvent(GdkEventKey *event); + bool onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter); + static void onKBShortcutRenderer(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + +private: + void themeChange(); + void symbolicThemeCheck(); + void toggleSymbolic(); + void changeIconsColors(); + void resetIconsColors(bool themechange = false); + void resetIconsColorsWrapper(); + void changeIconsColor(guint32 /*color*/); + void get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, guint32 &colorsetwarning, + guint32 &colorseterror); + + InkscapePreferences(); + InkscapePreferences(InkscapePreferences const &d); + InkscapePreferences operator=(InkscapePreferences const &d); + bool _init; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H + +/* + 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 : diff --git a/src/ui/dialog/input.cpp b/src/ui/dialog/input.cpp new file mode 100644 index 0000000..438914d --- /dev/null +++ b/src/ui/dialog/input.cpp @@ -0,0 +1,1792 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Input devices dialog (new) - implementation. + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device-manager.h" +#include "preferences.h" + +#include "input.h" + +/* XPM */ +static char const * core_xpm[] = { +"16 16 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" ", +" ", +" .++++++. ", +" +@+@@+@+ ", +" +@+@@+@+ ", +" +.+..+.+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" .++++++. ", +" ", +" ", +" "}; + +/* XPM */ +static char const *eraser[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOO OO", +"OOOOOOOOOOOO . O", +"OOOOOOOOOOO . OO", +"OOOOOOOOOO . OOO", +"OOOOOOOOO . OOOO", +"OOOOOOOO . OOOOO", +"OOOOOOOXo OOOOOO", +"OOOOOOXoXOOOOOOO", +"OOOOOXoXOOOOOOOO", +"OOOOXoXOOOOOOOOO", +"OOOXoXOOOOOOOOOO", +"OOXoXOOOOOOOOOOO", +"OOXXOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *mouse[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXX XXXXXXX", +"XXXXX . XXXXXXX", +"XXXX .... XXXXXX", +"XXXX .... XXXXXX", +"XXXXX .... XXXXX", +"XXXXX .... XXXXX", +"XXXXXX .... XXXX", +"XXXXXX .... XXXX", +"XXXXXXX . XXXXX", +"XXXXXXX XXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *pen[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXX XX", +"XXXXXXXXXXXX . X", +"XXXXXXXXXXX . XX", +"XXXXXXXXXX . XXX", +"XXXXXXXXX . XXXX", +"XXXXXXXX . XXXXX", +"XXXXXXX . XXXXXX", +"XXXXXX . XXXXXXX", +"XXXXX . XXXXXXXX", +"XXXX . XXXXXXXXX", +"XXX . XXXXXXXXXX", +"XX . XXXXXXXXXXX", +"XX XXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *sidebuttons[] = { +/* columns rows colors chars-per-pixel */ +"16 16 4 1", +" c black", +". c #808080", +"o c green", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"O..............O", +"O.OOOOOOOOOOOO.O", +"O OOOOOOOO O", +"O o OOOOOOOO o O", +"O o OOOOOOOO o O", +"O OOOOOOOO O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O..............O", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *tablet[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"X X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X X", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *tip[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOXOO", +"OOOOOOOOOOOOXoXO", +"OOOOOOOOOOOXoXOO", +"OOOOOOOOOOXoXOOO", +"OOOOOOOOOXoXOOOO", +"OOOOOOOOXoXOOOOO", +"OOOOOOO oXOOOOOO", +"OOOOOO . OOOOOOO", +"OOOOO . OOOOOOOO", +"OOOO . OOOOOOOOO", +"OOO . OOOOOOOOOO", +"OO . OOOOOOOOOOO", +"OO OOOOOOOOOOOO", +"OOOOXXXXXOOOOOOO", +"OOOOOOOOOXXXXXOO" +}; + +/* XPM */ +static char const *button_none[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c #808080", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX .. XX", +"X .XX. X", +"X.XX X.X", +"X.X XX.X", +"X .XX. X", +"XX .. XX", +"XXXXXXXX" +}; +/* XPM */ +static char const *button_off[] = { +/* columns rows colors chars-per-pixel */ +"8 8 4 1", +" c black", +". c #808080", +"X c gray100", +"o c None", +/* pixels */ +"oooooooo", +"oo. .oo", +"o. XX .o", +"o XXXX o", +"o XXXX o", +"o. XX .o", +"oo. .oo", +"oooooooo" +}; +/* XPM */ +static char const *button_on[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c green", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX XX", +"X .. X", +"X .... X", +"X .... X", +"X .. X", +"XX XX", +"XXXXXXXX" +}; + +/* XPM */ +static char const * axis_none_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #808080", +" ", +" .++++++++++++++++++. ", +" .+ . .+. ", +" + . . . + ", +" + . . . + ", +" .+. . +. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_off_xpm[] = { +"24 8 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" .++++++++++++++++++. ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_on_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #00FF00", +" ", +" .................... ", +" ..++++++++++++++++++.. ", +" .++++++++++++++++++++. ", +" .++++++++++++++++++++. ", +" ..++++++++++++++++++.. ", +" .................... ", +" "}; + +using Inkscape::InputDevice; + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +class DeviceModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn toggler; + Gtk::TreeModelColumn expander; + Gtk::TreeModelColumn description; + Gtk::TreeModelColumn > thumbnail; + Gtk::TreeModelColumn > device; + Gtk::TreeModelColumn mode; + + DeviceModelColumns() { add(toggler), add(expander), add(description); add(thumbnail); add(device); add(mode); } +}; + +static std::map &getModeToString() +{ + static std::map mapping; + if (mapping.empty()) { + mapping[Gdk::MODE_DISABLED] = _("Disabled"); + mapping[Gdk::MODE_SCREEN] = C_("Input device", "Screen"); + mapping[Gdk::MODE_WINDOW] = _("Window"); + } + + return mapping; +} + +static int getModeId(Gdk::InputMode im) +{ + if (im == Gdk::MODE_DISABLED) return 0; + if (im == Gdk::MODE_SCREEN) return 1; + if (im == Gdk::MODE_WINDOW) return 2; + + return 0; +} + +static std::map &getStringToMode() +{ + static std::map mapping; + if (mapping.empty()) { + mapping[_("Disabled")] = Gdk::MODE_DISABLED; + mapping[_("Screen")] = Gdk::MODE_SCREEN; + mapping[_("Window")] = Gdk::MODE_WINDOW; + } + + return mapping; +} + + + +class InputDialogImpl : public InputDialog { +public: + InputDialogImpl(); + ~InputDialogImpl() override = default; + +private: + class ConfPanel : public Gtk::VBox + { + public: + ConfPanel(); + ~ConfPanel() override; + + class Blink : public Preferences::Observer + { + public: + Blink(ConfPanel &parent); + ~Blink() override; + void notify(Preferences::Entry const &new_val) override; + + ConfPanel &parent; + }; + + static void commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr store); + static void setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + static void commitCellStateChange(Glib::ustring const &path, Glib::RefPtr store); + static void setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + void saveSettings(); + void onTreeSelect(); + void useExtToggled(); + + void onModeChange(); + void setKeys(gint count); + void setAxis(gint count); + + Glib::RefPtr confDeviceStore; + Gtk::TreeIter confDeviceIter; + Gtk::TreeView confDeviceTree; + Gtk::ScrolledWindow confDeviceScroller; + Blink watcher; + Gtk::CheckButton useExt; + Gtk::Button save; + Gtk::Paned pane; + Gtk::VBox detailsBox; + Gtk::HBox titleFrame; + Gtk::Label titleLabel; + Inkscape::UI::Widget::Frame axisFrame; + Inkscape::UI::Widget::Frame keysFrame; + Gtk::VBox axisVBox; + Gtk::ComboBoxText modeCombo; + Gtk::Label modeLabel; + Gtk::HBox modeBox; + + class KeysColumns : public Gtk::TreeModel::ColumnRecord + { + public: + KeysColumns() + { + add(name); + add(value); + } + ~KeysColumns() override = default; + + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn value; + }; + + KeysColumns keysColumns; + KeysColumns axisColumns; + + Glib::RefPtr axisStore; + Gtk::TreeView axisTree; + Gtk::ScrolledWindow axisScroll; + + Glib::RefPtr keysStore; + Gtk::TreeView keysTree; + Gtk::ScrolledWindow keysScroll; + Gtk::CellRendererAccel _kb_shortcut_renderer; + + + }; + + static DeviceModelColumns &getCols(); + + enum PixId {PIX_CORE, PIX_PEN, PIX_MOUSE, PIX_TIP, PIX_TABLET, PIX_ERASER, PIX_SIDEBUTTONS, + PIX_BUTTONS_NONE, PIX_BUTTONS_ON, PIX_BUTTONS_OFF, + PIX_AXIS_NONE, PIX_AXIS_ON, PIX_AXIS_OFF}; + + static Glib::RefPtr getPix(PixId id); + + std::map > buttonMap; + std::map > > axesMap; + + GdkInputSource lastSourceSeen; + Glib::ustring lastDevnameSeen; + + Glib::RefPtr deviceStore; + Gtk::TreeIter deviceIter; + Gtk::TreeView deviceTree; + Inkscape::UI::Widget::Frame testFrame; + Inkscape::UI::Widget::Frame axisFrame; + Gtk::ScrolledWindow treeScroller; + Gtk::ScrolledWindow detailScroller; + Gtk::Paned splitter; + Gtk::Paned split2; + Gtk::Label devName; + Gtk::Label devKeyCount; + Gtk::Label devAxesCount; + Gtk::ComboBoxText axesCombo; + Gtk::ProgressBar axesValues[6]; + Gtk::Grid axisTable; + Gtk::ComboBoxText buttonCombo; + Gtk::ComboBoxText linkCombo; + sigc::connection linkConnection; + Gtk::Label keyVal; + Gtk::Entry keyEntry; + Gtk::Notebook topHolder; + Gtk::Image testThumb; + Gtk::Image testButtons[24]; + Gtk::Image testAxes[8]; + Gtk::Grid imageTable; + Gtk::EventBox testDetector; + + ConfPanel cfgPanel; + + + static void setupTree( Glib::RefPtr store, Gtk::TreeIter &tablet ); + void setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ); + void updateTestButtons( Glib::ustring const& key, gint hotButton ); + void updateTestAxes( Glib::ustring const& key, GdkDevice* dev ); + void mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev); + Glib::ustring getKeyFor( GdkDevice* device ); + bool eventSnoop(GdkEvent* event); + void linkComboChanged(); + void resyncToSelection(); + void handleDeviceChange(Glib::RefPtr device); + void updateDeviceAxes(Glib::RefPtr device); + void updateDeviceButtons(Glib::RefPtr device); + static void updateDeviceLinks(Glib::RefPtr device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree); + + static bool findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result); + static bool findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result); + +}; // class InputDialogImpl + + +DeviceModelColumns &InputDialogImpl::getCols() +{ + static DeviceModelColumns cols; + return cols; +} + +Glib::RefPtr InputDialogImpl::getPix(PixId id) +{ + static std::map > mappings; + + mappings[PIX_CORE] = Gdk::Pixbuf::create_from_xpm_data(core_xpm); + mappings[PIX_PEN] = Gdk::Pixbuf::create_from_xpm_data(pen); + mappings[PIX_MOUSE] = Gdk::Pixbuf::create_from_xpm_data(mouse); + mappings[PIX_TIP] = Gdk::Pixbuf::create_from_xpm_data(tip); + mappings[PIX_TABLET] = Gdk::Pixbuf::create_from_xpm_data(tablet); + mappings[PIX_ERASER] = Gdk::Pixbuf::create_from_xpm_data(eraser); + mappings[PIX_SIDEBUTTONS] = Gdk::Pixbuf::create_from_xpm_data(sidebuttons); + + mappings[PIX_BUTTONS_NONE] = Gdk::Pixbuf::create_from_xpm_data(button_none); + mappings[PIX_BUTTONS_ON] = Gdk::Pixbuf::create_from_xpm_data(button_on); + mappings[PIX_BUTTONS_OFF] = Gdk::Pixbuf::create_from_xpm_data(button_off); + + mappings[PIX_AXIS_NONE] = Gdk::Pixbuf::create_from_xpm_data(axis_none_xpm); + mappings[PIX_AXIS_ON] = Gdk::Pixbuf::create_from_xpm_data(axis_on_xpm); + mappings[PIX_AXIS_OFF] = Gdk::Pixbuf::create_from_xpm_data(axis_off_xpm); + + Glib::RefPtr pix; + if (mappings.find(id) != mappings.end()) { + pix = mappings[id]; + } + + return pix; +} + + +// Now that we've defined the *Impl class, we can do the method to acquire one. +InputDialog &InputDialog::getInstance() +{ + InputDialog *dialog = new InputDialogImpl(); + return *dialog; +} + + +InputDialogImpl::InputDialogImpl() : + InputDialog(), + + lastSourceSeen((GdkInputSource)-1), + lastDevnameSeen(""), + deviceStore(Gtk::TreeStore::create(getCols())), + deviceIter(), + deviceTree(deviceStore), + testFrame(_("Test Area")), + axisFrame(_("Axis")), + treeScroller(), + detailScroller(), + splitter(), + split2(Gtk::ORIENTATION_VERTICAL), + axisTable(), + linkCombo(), + topHolder(), + imageTable(), + testDetector(), + cfgPanel() +{ + Gtk::Box *contents = _getContents(); + + treeScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + treeScroller.set_shadow_type(Gtk::SHADOW_IN); + treeScroller.add(deviceTree); + treeScroller.set_size_request(50, 0); + + split2.pack1(axisFrame, false, false); + split2.pack2(testFrame, true, true); + + splitter.pack1(treeScroller); + splitter.pack2(split2); + + testDetector.add(imageTable); + testFrame.add(testDetector); + testThumb.set(getPix(PIX_TABLET)); + testThumb.set_margin_top(24); + testThumb.set_margin_bottom(24); + testThumb.set_margin_start(24); + testThumb.set_margin_end(24); + testThumb.set_hexpand(); + testThumb.set_vexpand(); + imageTable.attach(testThumb, 0, 0, 8, 1); + + { + guint col = 0; + guint row = 1; + for (auto & testButton : testButtons) { + testButton.set(getPix(PIX_BUTTONS_NONE)); + imageTable.attach(testButton, col, row, 1, 1); + col++; + if (col > 7) { + col = 0; + row++; + } + } + + col = 0; + for (auto & testAxe : testAxes) { + testAxe.set(getPix(PIX_AXIS_NONE)); + imageTable.attach(testAxe, col * 2, row, 2, 1); + col++; + if (col > 3) { + col = 0; + row++; + } + } + } + + + // This is a hidden preference to enable the "hardware" details in a separate tab + // By default this is not available to users + if (Preferences::get()->getBool("/dialogs/inputdevices/test")) { + topHolder.append_page(cfgPanel, _("Configuration")); + topHolder.append_page(splitter, _("Hardware")); + topHolder.show_all(); + topHolder.set_current_page(0); + contents->pack_start(topHolder); + } else { + contents->pack_start(cfgPanel); + } + + + int rowNum = 0; + + axisFrame.add(axisTable); + + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(_("Link:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + linkCombo.append(_("None")); + linkCombo.set_active_text(_("None")); + linkCombo.set_sensitive(false); + linkConnection = linkCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::linkComboChanged)); + axisTable.attach(linkCombo, 1, rowNum, 1, 1); + rowNum++; + + lbl = Gtk::manage(new Gtk::Label(_("Axes count:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devAxesCount, 1, rowNum, 1, 1); + rowNum++; + + for (auto & axesValue : axesValues) { + lbl = Gtk::manage(new Gtk::Label(_("axis:"))); + lbl->set_hexpand(); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + + axesValue.set_hexpand(); + axisTable.attach(axesValue, 1, rowNum, 1, 1); + axesValue.set_sensitive(false); + + rowNum++; + + + } + + lbl = Gtk::manage(new Gtk::Label(_("Button count:"))); + + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devKeyCount, 1, rowNum, 1, 1); + + rowNum++; + + axisTable.attach(keyVal, 0, rowNum, 2, 1); + + rowNum++; + + testDetector.signal_event().connect(sigc::mem_fun(*this, &InputDialogImpl::eventSnoop)); + + // TODO: Extension event stuff has been removed from public API in GTK+ 3 + // Need to check that this hasn't broken anything + testDetector.add_events(Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK |Gdk::PROXIMITY_IN_MASK|Gdk::PROXIMITY_OUT_MASK|Gdk::SCROLL_MASK); + + axisTable.attach(keyEntry, 0, rowNum, 2, 1); + + rowNum++; + + + axisTable.set_sensitive(false); + +//- 16x16/devices +// gnome-dev-mouse-optical +// input-mouse +// input-tablet +// mouse + + //Add the TreeView's view columns: + deviceTree.append_column("I", getCols().thumbnail); + deviceTree.append_column("Bar", getCols().description); + + deviceTree.set_enable_tree_lines(); + deviceTree.set_headers_visible(false); + deviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::resyncToSelection)); + + + setupTree( deviceStore, deviceIter ); + + Inkscape::DeviceManager::getManager().signalDeviceChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::handleDeviceChange)); + Inkscape::DeviceManager::getManager().signalAxesChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceAxes)); + Inkscape::DeviceManager::getManager().signalButtonsChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceButtons)); + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), deviceIter, &deviceTree)); + + deviceTree.expand_all(); + show_all_children(); +} + +class TabletTmp { +public: + TabletTmp() = default; + + Glib::ustring name; + std::list > devices; +}; + +static Glib::ustring getCommon( std::list const &names ) +{ + Glib::ustring result; + + if ( !names.empty() ) { + size_t pos = 0; + bool match = true; + while ( match ) { + if ( names.begin()->length() > pos ) { + gunichar ch = (*names.begin())[pos]; + for (const auto & name : names) { + if ( (pos >= name.length()) + || (name[pos] != ch) ) { + match = false; + break; + } + } + if (match) { + result += ch; + pos++; + } + } else { + match = false; + } + } + } + + return result; +} + + +void InputDialogImpl::ConfPanel::onModeChange() +{ + Glib::ustring newText = modeCombo.get_active_text(); + + Glib::RefPtr sel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = sel->get_selected(); + if (iter) { + Glib::RefPtr dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + +} + + +void InputDialogImpl::setupTree( Glib::RefPtr store, Gtk::TreeIter &tablet ) +{ + std::list > devList = Inkscape::DeviceManager::getManager().getDevices(); + if ( !devList.empty() ) { + //Gtk::TreeModel::Row row = *(store->append()); + //row[getCols().description] = _("Hardware"); + + // Let's make some tablets!!! + std::list tablets; + std::set consumed; + + // Phase 1 - figure out which tablets are present + for (auto dev : devList) { + if ( dev ) { + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + consumed.insert( dev->getId() ); + if ( tablets.empty() ) { + TabletTmp tmp; + tablets.push_back(tmp); + } + tablets.back().devices.push_back(dev); + } + } else { + g_warning("Null device in list"); + } + } + + // Phase 2 - build a UI for the present devices + for (auto & it : tablets) { + tablet = store->prepend(/*row.children()*/); + Gtk::TreeModel::Row childrow = *tablet; + if ( it.name.empty() ) { + // Check to see if we can derive one + std::list names; + for (auto & device : it.devices) { + names.push_back( device->getName() ); + } + Glib::ustring common = getCommon(names); + if ( !common.empty() ) { + it.name = common; + } + } + childrow[getCols().description] = it.name.empty() ? _("Tablet") : it.name ; + childrow[getCols().thumbnail] = getPix(PIX_TABLET); + + // Check if there is an eraser we can link to a pen + for ( std::list >::iterator it2 = it.devices.begin(); it2 != it.devices.end(); ++it2 ) { + Glib::RefPtr dev = *it2; + if ( dev->getSource() == Gdk::SOURCE_PEN ) { + for (auto dev2 : it.devices) { + if ( dev2->getSource() == Gdk::SOURCE_ERASER ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), dev2->getId()); + break; // only check the first eraser... for now + } + break; // only check the first pen... for now + } + } + } + + for (auto dev : it.devices) { + Gtk::TreeModel::Row deviceRow = *(store->append(childrow.children())); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + switch ( dev->getSource() ) { + case Gdk::SOURCE_MOUSE: + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + break; + case Gdk::SOURCE_PEN: + if (deviceRow[getCols().description] == _("pad")) { + deviceRow[getCols().thumbnail] = getPix(PIX_SIDEBUTTONS); + } else { + deviceRow[getCols().thumbnail] = getPix(PIX_TIP); + } + break; + case Gdk::SOURCE_CURSOR: + deviceRow[getCols().thumbnail] = getPix(PIX_MOUSE); + break; + case Gdk::SOURCE_ERASER: + deviceRow[getCols().thumbnail] = getPix(PIX_ERASER); + break; + default: + ; // nothing + } + } + } + + for (auto dev : devList) { + if ( dev && (consumed.find( dev->getId() ) == consumed.end()) ) { + Gtk::TreeModel::Row deviceRow = *(store->prepend(/*row.children()*/)); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + } + } + + } else { + g_warning("No devices found"); + } +} + + +InputDialogImpl::ConfPanel::ConfPanel() : + Gtk::VBox(), + confDeviceStore(Gtk::TreeStore::create(getCols())), + confDeviceIter(), + confDeviceTree(confDeviceStore), + confDeviceScroller(), + watcher(*this), + useExt(_("_Use pressure-sensitive tablet (requires restart)"), true), + save(_("_Save"), true), + detailsBox(false, 4), + titleFrame(false, 4), + titleLabel(""), + axisFrame(_("Axes")), + keysFrame(_("Keys")), + modeLabel(_("Mode:")), + modeBox(false, 4) + +{ + + + confDeviceScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + confDeviceScroller.set_shadow_type(Gtk::SHADOW_IN); + confDeviceScroller.add(confDeviceTree); + confDeviceScroller.set_size_request(120, 0); + + /* class Foo : public Gtk::TreeModel::ColumnRecord { + public : + Gtk::TreeModelColumn one; + Foo() {add(one);} + }; + static Foo foo; + + //Add the TreeView's view columns: + { + Gtk::CellRendererToggle *rendr = new Gtk::CellRendererToggle(); + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("xx", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setCellStateToggle)); + rendr->signal_toggled().connect(sigc::bind(sigc::ptr_fun(commitCellStateChange), confDeviceStore)); + } + }*/ + + //int expPos = confDeviceTree.append_column("", getCols().expander); + + confDeviceTree.append_column("I", getCols().thumbnail); + confDeviceTree.append_column("Bar", getCols().description); + + //confDeviceTree.get_column(0)->set_fixed_width(100); + //confDeviceTree.get_column(1)->set_expand(); + +/* { + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("X", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setModeCellString)); + rendr->signal_edited().connect(sigc::bind(sigc::ptr_fun(commitCellModeChange), confDeviceStore)); + rendr->property_editable() = true; + } + }*/ + + //confDeviceTree.set_enable_tree_lines(); + confDeviceTree.property_enable_tree_lines() = false; + confDeviceTree.property_enable_grid_lines() = false; + confDeviceTree.set_headers_visible(false); + //confDeviceTree.set_expander_column( *confDeviceTree.get_column(expPos - 1) ); + + confDeviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onTreeSelect)); + + setupTree( confDeviceStore, confDeviceIter ); + + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), confDeviceIter, &confDeviceTree)); + + confDeviceTree.expand_all(); + + useExt.set_active(Preferences::get()->getBool("/options/useextinput/value")); + useExt.signal_toggled().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::useExtToggled)); + + auto buttonBox = Gtk::manage(new Gtk::ButtonBox); + buttonBox->set_layout (Gtk::BUTTONBOX_END); + //Gtk::Alignment *align = new Gtk::Alignment(Gtk::ALIGN_END, Gtk::ALIGN_START, 0, 0); + buttonBox->add(save); + save.signal_clicked().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::saveSettings)); + + titleFrame.pack_start(titleLabel, true, true); + //titleFrame.set_shadow_type(Gtk::SHADOW_IN); + + modeCombo.append(getModeToString()[Gdk::MODE_DISABLED]); + modeCombo.append(getModeToString()[Gdk::MODE_SCREEN]); + modeCombo.append(getModeToString()[Gdk::MODE_WINDOW]); + modeCombo.set_tooltip_text(_("A device can be 'Disabled', its co-ordinates mapped to the whole 'Screen', or to a single (usually focused) 'Window'")); + modeCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onModeChange)); + + modeBox.pack_start(modeLabel, false, false); + modeBox.pack_start(modeCombo, true, true); + + axisVBox.add(axisScroll); + axisFrame.add(axisVBox); + + keysFrame.add(keysScroll); + + /** + * Scrolled Window + */ + keysScroll.add(keysTree); + keysScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + keysScroll.set_shadow_type(Gtk::SHADOW_IN); + keysScroll.set_size_request(120, 80); + + keysStore = Gtk::ListStore::create(keysColumns); + + _kb_shortcut_renderer.property_editable() = true; + + keysTree.set_model(keysStore); + keysTree.set_headers_visible(false); + keysTree.append_column("Name", keysColumns.name); + keysTree.append_column("Value", keysColumns.value); + + //keysTree.append_column("Value", _kb_shortcut_renderer); + //keysTree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), keysColumns.value); + //_kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeEdited) ); + //_kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeCleared) ); + + axisStore = Gtk::ListStore::create(axisColumns); + + axisTree.set_model(axisStore); + axisTree.set_headers_visible(false); + axisTree.append_column("Name", axisColumns.name); + axisTree.append_column("Value", axisColumns.value); + + /** + * Scrolled Window + */ + axisScroll.add(axisTree); + axisScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + axisScroll.set_shadow_type(Gtk::SHADOW_IN); + axisScroll.set_size_request(0, 150); + + pane.pack1(confDeviceScroller); + pane.pack2(detailsBox); + + detailsBox.pack_start(titleFrame, false, false, 6); + detailsBox.pack_start(modeBox, false, false, 6); + detailsBox.pack_start(axisFrame, false, false); + detailsBox.pack_start(keysFrame, false, false); + detailsBox.set_border_width(4); + + pack_start(pane, true, true); + pack_start(useExt, Gtk::PACK_SHRINK); + pack_start(*buttonBox, false, false); + + // Select the first device + confDeviceTree.get_selection()->select(confDeviceStore->get_iter("0")); + +} + +InputDialogImpl::ConfPanel::~ConfPanel() += default; + +void InputDialogImpl::ConfPanel::setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererCombo *combo = dynamic_cast(rndr); + if (combo) { + Glib::RefPtr dev = (*iter)[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (dev && (getModeToString().find(mode) != getModeToString().end())) { + combo->property_text() = getModeToString()[mode]; + } else { + combo->property_text() = ""; + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + + +} + +void InputDialogImpl::ConfPanel::setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererToggle *toggle = dynamic_cast(rndr); + if (toggle) { + Glib::RefPtr dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + toggle->set_active(mode != Gdk::MODE_DISABLED); + } else { + toggle->set_active(false); + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellStateChange(Glib::ustring const &path, Glib::RefPtr store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (mode == Gdk::MODE_DISABLED) { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_SCREEN ); + } else { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_DISABLED ); + } + } + } +} + +void InputDialogImpl::ConfPanel::onTreeSelect() +{ + Glib::RefPtr treeSel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr dev = row[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + modeCombo.set_active(getModeId(mode)); + + titleLabel.set_markup("" + row[getCols().description] + ""); + + if (dev) { + setKeys(dev->getNumKeys()); + setAxis(dev->getNumAxes()); + } + } +} +void InputDialogImpl::ConfPanel::saveSettings() +{ + Inkscape::DeviceManager::getManager().saveConfig(); +} + +void InputDialogImpl::ConfPanel::useExtToggled() +{ + bool active = useExt.get_active(); + if (active != Preferences::get()->getBool("/options/useextinput/value")) { + Preferences::get()->setBool("/options/useextinput/value", active); + if (active) { + // As a work-around for a common problem, enable tablet toggles on the calligraphic tool. + // Covered in Launchpad bug #196195. + Preferences::get()->setBool("/tools/tweak/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usetilt", true); + } + } +} + +InputDialogImpl::ConfPanel::Blink::Blink(ConfPanel &parent) : + Preferences::Observer("/options/useextinput/value"), + parent(parent) +{ + Preferences::get()->addObserver(*this); +} + +InputDialogImpl::ConfPanel::Blink::~Blink() +{ + Preferences::get()->removeObserver(*this); +} + +void InputDialogImpl::ConfPanel::Blink::notify(Preferences::Entry const &new_val) +{ + parent.useExt.set_active(new_val.getBool()); +} + +void InputDialogImpl::handleDeviceChange(Glib::RefPtr device) +{ +// g_message("OUCH!!!! for %p hits %s", &device, device->getId().c_str()); + std::vector > stores; + stores.push_back(deviceStore); + stores.push_back(cfgPanel.confDeviceStore); + + for (auto & store : stores) { + Gtk::TreeModel::iterator deviceIter; + store->foreach_iter( sigc::bind( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + if ( deviceIter ) { + Gdk::InputMode mode = device->getMode(); + Gtk::TreeModel::Row row = *deviceIter; + if (row[getCols().mode] != mode) { + row[getCols().mode] = mode; + } + } + } +} + +void InputDialogImpl::updateDeviceAxes(Glib::RefPtr device) +{ + gint live = device->getLiveAxes(); + + std::map > existing = axesMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( (existing.find(num) == existing.end()) || (existing[num].first < 2) ) { + axesMap[device->getId()][num].first = 2; + axesMap[device->getId()][num].second = 0.0; + } + } + } + updateTestAxes( device->getId(), nullptr ); +} + +void InputDialogImpl::updateDeviceButtons(Glib::RefPtr device) +{ + gint live = device->getLiveButtons(); + std::set existing = buttonMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( existing.find(num) == existing.end() ) { + buttonMap[device->getId()].insert(num); + } + } + } + updateTestButtons(device->getId(), -1); +} + + +bool InputDialogImpl::findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr dev = (*iter)[getCols().device]; + if ( dev && (dev->getId() == id) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +bool InputDialogImpl::findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr dev = (*iter)[getCols().device]; + if ( dev && (dev->getLink() == link) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +void InputDialogImpl::updateDeviceLinks(Glib::RefPtr device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree) +{ + Glib::RefPtr deviceStore = Glib::RefPtr::cast_dynamic(tree->get_model()); + +// g_message("Links!!!! for %p hits [%s] with link of [%s]", &device, device->getId().c_str(), device->getLink().c_str()); + Gtk::TreeModel::iterator deviceIter; + deviceStore->foreach_iter( sigc::bind( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + + if ( deviceIter ) { + // Found the device concerned. Can proceed. + + if ( device->getLink().empty() ) { + // is now unlinked +// g_message("Item %s is unlinked", device->getId().c_str()); + if ( deviceIter->parent() != tabletIter ) { + // Not the child of the tablet. move on up + + Glib::RefPtr dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(tabletIter->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + } else { + // is linking + if ( deviceIter->parent() == tabletIter ) { + // Simple case. Not already linked + + Gtk::TreeIter newGroup = deviceStore->append(tabletIter->children()); + (*newGroup)[getCols().description] = _("Pen"); + (*newGroup)[getCols().thumbnail] = getPix(PIX_PEN); + + Glib::RefPtr dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + + Gtk::TreeModel::iterator linkIter; + deviceStore->foreach_iter( sigc::bind( + sigc::ptr_fun(&InputDialogImpl::findDeviceByLink), + device->getId(), + &linkIter) ); + if ( linkIter ) { + dev = (*linkIter)[getCols().device]; + descr = (*linkIter)[getCols().description]; + thumb = (*linkIter)[getCols().thumbnail]; + + deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + Gtk::TreeModel::iterator oldParent = linkIter->parent(); + deviceStore->erase(linkIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + tree->expand_row(Gtk::TreePath(newGroup), true); + } + } + } +} + +void InputDialogImpl::linkComboChanged() { + Glib::RefPtr treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr dev = row[getCols().device]; + if ( dev ) { + if ( linkCombo.get_active_row_number() == 0 ) { + // It is the "None" entry + DeviceManager::getManager().setLinkedTo(dev->getId(), ""); + } else { + Glib::ustring linkName = linkCombo.get_active_text(); + std::list > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( linkName == (*it)->getName() ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), (*it)->getId()); + break; + } + } + } + } + } +} + +void InputDialogImpl::resyncToSelection() { + bool clear = true; + Glib::RefPtr treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr dev = row[getCols().device]; + + if ( dev ) { + axisTable.set_sensitive(true); + + linkConnection.block(); + linkCombo.remove_all(); + linkCombo.append(_("None")); + linkCombo.set_active(0); + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + Glib::ustring linked = dev->getLink(); + std::list > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( ((*it)->getSource() != Gdk::SOURCE_MOUSE) && ((*it) != dev) ) { + linkCombo.append((*it)->getName().c_str()); + if ( (linked.length() > 0) && (linked == (*it)->getId()) ) { + linkCombo.set_active_text((*it)->getName().c_str()); + } + } + } + linkCombo.set_sensitive(true); + } else { + linkCombo.set_sensitive(false); + } + linkConnection.unblock(); + + clear = false; + devName.set_label(row[getCols().description]); + axisFrame.set_label(row[getCols().description]); + setupValueAndCombo( dev->getNumAxes(), dev->getNumAxes(), devAxesCount, axesCombo); + setupValueAndCombo( dev->getNumKeys(), dev->getNumKeys(), devKeyCount, buttonCombo); + + + } + } + + axisTable.set_sensitive(!clear); + if (clear) { + axisFrame.set_label(""); + devName.set_label(""); + devAxesCount.set_label(""); + devKeyCount.set_label(""); + } +} + +void InputDialogImpl::ConfPanel::setAxis(gint count) +{ + /* + * TODO - Make each axis editable + */ + axisStore->clear(); + + static Glib::ustring axesLabels[6] = {_("X"), _("Y"), _("Pressure"), _("X tilt"), _("Y tilt"), _("Wheel")}; + + for ( gint barNum = 0; barNum < static_cast(G_N_ELEMENTS(axesLabels)); barNum++ ) { + + Gtk::TreeModel::Row row = *(axisStore->append()); + row[axisColumns.name] = axesLabels[barNum]; + if (barNum < count) { + row[axisColumns.value] = Glib::ustring::format(barNum+1); + } else { + row[axisColumns.value] = C_("Input device axe", "None"); + } + } + +} +void InputDialogImpl::ConfPanel::setKeys(gint count) +{ + /* + * TODO - Make each key assignable + */ + + keysStore->clear(); + + for (gint i = 0; i < count; i++) { + Gtk::TreeModel::Row row = *(keysStore->append()); + row[keysColumns.name] = Glib::ustring::format(i+1); + row[keysColumns.value] = _("Disabled"); + } + + +} +void InputDialogImpl::setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ) +{ + gchar *tmp = g_strdup_printf("%d", reported); + label.set_label(tmp); + g_free(tmp); + + combo.remove_all(); + for ( gint i = 1; i <= reported; ++i ) { + tmp = g_strdup_printf("%d", i); + combo.append(tmp); + g_free(tmp); + } + + if ( (1 <= actual) && (actual <= reported) ) { + combo.set_active(actual - 1); + } +} + +void InputDialogImpl::updateTestButtons( Glib::ustring const& key, gint hotButton ) +{ + for ( gint i = 0; i < static_cast(G_N_ELEMENTS(testButtons)); i++ ) { + if ( buttonMap[key].find(i) != buttonMap[key].end() ) { + if ( i == hotButton ) { + testButtons[i].set(getPix(PIX_BUTTONS_ON)); + } else { + testButtons[i].set(getPix(PIX_BUTTONS_OFF)); + } + } else { + testButtons[i].set(getPix(PIX_BUTTONS_NONE)); + } + } +} + +void InputDialogImpl::updateTestAxes( Glib::ustring const& key, GdkDevice* dev ) +{ + //static gdouble epsilon = 0.0001; + { + Glib::RefPtr treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr idev = row[getCols().device]; + if ( !idev || (idev->getId() != key) ) { + dev = nullptr; + } + } + } + + for ( gint i = 0; i < static_cast(G_N_ELEMENTS(testAxes)); i++ ) { + if ( axesMap[key].find(i) != axesMap[key].end() ) { + switch ( axesMap[key][i].first ) { + case 0: + case 1: + testAxes[i].set(getPix(PIX_AXIS_NONE)); + if ( dev && (i < static_cast(G_N_ELEMENTS(axesValues)) ) ) { + axesValues[i].set_sensitive(false); + } + break; + case 2: + testAxes[i].set(getPix(PIX_AXIS_OFF)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast(G_N_ELEMENTS(axesValues)) ) ) { + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + break; + case 3: + testAxes[i].set(getPix(PIX_AXIS_ON)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast(G_N_ELEMENTS(axesValues)) ) ) { + + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + } + + } else { + testAxes[i].set(getPix(PIX_AXIS_NONE)); + } + } + if ( !dev ) { + for (auto & axesValue : axesValues) { + axesValue.set_fraction(0.0); + axesValue.set_text(""); + axesValue.set_sensitive(false); + } + } +} + +void InputDialogImpl::mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev ) +{ + guint numAxes = gdk_device_get_n_axes(dev); + + static gdouble epsilon = 0.0001; + if ( (numAxes > 0) && axes) { + for ( guint axisNum = 0; axisNum < numAxes; axisNum++ ) { + // 0 == new, 1 == set value, 2 == changed value, 3 == active + gdouble diff = axesMap[key][axisNum].second - axes[axisNum]; + switch(axesMap[key][axisNum].first) { + case 0: + { + axesMap[key][axisNum].first = 1; + axesMap[key][axisNum].second = axes[axisNum]; + } + break; + case 1: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { +// g_message("Axis %d changed on %s]", axisNum, key.c_str()); + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + DeviceManager::getManager().addAxis(key, axisNum); + } + } + break; + case 2: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + } + } + break; + case 3: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].second = axes[axisNum]; + } else { + axesMap[key][axisNum].first = 2; + updateTestAxes(key, dev); + } + } + } + } + } + // std::map > > axesMap; +} + +Glib::ustring InputDialogImpl::getKeyFor( GdkDevice* device ) +{ + Glib::ustring key; + + GdkInputSource source = gdk_device_get_source(device); + const gchar *name = gdk_device_get_name(device); + + switch ( source ) { + case GDK_SOURCE_MOUSE: + key = "M:"; + break; + case GDK_SOURCE_CURSOR: + key = "C:"; + break; + case GDK_SOURCE_PEN: + key = "P:"; + break; + case GDK_SOURCE_ERASER: + key = "E:"; + break; + default: + key = "?:"; + } + key += name; + + return key; +} + +bool InputDialogImpl::eventSnoop(GdkEvent* event) +{ + int modmod = 0; + + GdkInputSource source = lastSourceSeen; + Glib::ustring devName = lastDevnameSeen; + Glib::ustring key; + gint hotButton = -1; + + switch ( event->type ) { + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + { + GdkEventKey* keyEvt = reinterpret_cast(event); + gchar* name = gtk_accelerator_name(keyEvt->keyval, static_cast(keyEvt->state)); + keyVal.set_label(name); +// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name); + g_free(name); + } + break; + case GDK_BUTTON_PRESS: + modmod = 1; + // fallthrough + case GDK_BUTTON_RELEASE: + { + GdkEventButton* btnEvt = reinterpret_cast(event); + if ( btnEvt->device ) { + key = getKeyFor(btnEvt->device); + source = gdk_device_get_source(btnEvt->device); + devName = gdk_device_get_name(btnEvt->device); + mapAxesValues(key, btnEvt->axes, btnEvt->device); + + if ( buttonMap[key].find(btnEvt->button) == buttonMap[key].end() ) { +// g_message("New button found for %s = %d", key.c_str(), btnEvt->button); + buttonMap[key].insert(btnEvt->button); + DeviceManager::getManager().addButton(key, btnEvt->button); + } + hotButton = modmod ? btnEvt->button : -1; + updateTestButtons(key, hotButton); + } + gchar* name = gtk_accelerator_name(0, static_cast(btnEvt->state)); + keyVal.set_label(name); +// g_message("%d BTN state:0x%08x %c %4d [%s] dev:%p [%s] ", +// btnEvt->type, btnEvt->state, +// (modmod ? '+':'-'), +// btnEvt->button, name, btnEvt->device, +// (btnEvt->device ? btnEvt->device->name : "null") + +// ); + g_free(name); + } + break; + case GDK_MOTION_NOTIFY: + { + GdkEventMotion* btnMtn = reinterpret_cast(event); + if ( btnMtn->device ) { + key = getKeyFor(btnMtn->device); + source = gdk_device_get_source(btnMtn->device); + devName = gdk_device_get_name(btnMtn->device); + mapAxesValues(key, btnMtn->axes, btnMtn->device); + } + gchar* name = gtk_accelerator_name(0, static_cast(btnMtn->state)); + keyVal.set_label(name); +// g_message("%d MOV state:0x%08x [%s] dev:%p [%s] %3.2f %3.2f %3.2f %3.2f %3.2f %3.2f", btnMtn->type, btnMtn->state, +// name, btnMtn->device, +// (btnMtn->device ? btnMtn->device->name : "null"), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 0)) ? btnMtn->axes[0]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 1)) ? btnMtn->axes[1]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 2)) ? btnMtn->axes[2]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 3)) ? btnMtn->axes[3]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 4)) ? btnMtn->axes[4]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 5)) ? btnMtn->axes[5]:0) +// ); + g_free(name); + } + break; + default: + ;// nothing + } + + + if ( (lastSourceSeen != source) || (lastDevnameSeen != devName) ) { + switch (source) { + case GDK_SOURCE_MOUSE: { + testThumb.set(getPix(PIX_CORE)); + break; + } + case GDK_SOURCE_CURSOR: { +// g_message("flip to cursor"); + testThumb.set(getPix(PIX_MOUSE)); + break; + } + case GDK_SOURCE_PEN: { + if (devName == _("pad")) { +// g_message("flip to pad"); + testThumb.set(getPix(PIX_SIDEBUTTONS)); + } else { +// g_message("flip to pen"); + testThumb.set(getPix(PIX_TIP)); + } + break; + } + case GDK_SOURCE_ERASER: { +// g_message("flip to eraser"); + testThumb.set(getPix(PIX_ERASER)); + break; + } + /// \fixme GTK3 added new GDK_SOURCEs that should be handled here! + case GDK_SOURCE_KEYBOARD: + case GDK_SOURCE_TOUCHSCREEN: + case GDK_SOURCE_TOUCHPAD: + case GDK_SOURCE_TRACKPOINT: + case GDK_SOURCE_TABLET_PAD: + g_warning("InputDialogImpl::eventSnoop : unhandled GDK_SOURCE type!"); + break; + } + + updateTestButtons(key, hotButton); + lastSourceSeen = source; + lastDevnameSeen = devName; + } + + return false; +} + + +} // end namespace Inkscape +} // end namespace UI +} // end namespace Dialog + + +/* + 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 : diff --git a/src/ui/dialog/input.h b/src/ui/dialog/input.h new file mode 100644 index 0000000..a756cc5 --- /dev/null +++ b/src/ui/dialog/input.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Input devices dialog (new) + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INPUT_H +#define INKSCAPE_UI_DIALOG_INPUT_H + + +#include "verbs.h" +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InputDialog : public UI::Widget::Panel +{ +public: + static InputDialog &getInstance(); + + InputDialog() : UI::Widget::Panel("/dialogs/inputdevices", SP_VERB_DIALOG_INPUT) {} + ~InputDialog() override = default; +}; + +} // namespace Dialog +} // namesapce UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_INPUT_H + +/* + 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 : diff --git a/src/ui/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp new file mode 100644 index 0000000..708c90e --- /dev/null +++ b/src/ui/dialog/knot-properties.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington + * Andrius R. + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/knot-properties.h" + +#include +#include +#include +#include "inkscape.h" +#include "util/units.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +KnotPropertiesDialog::KnotPropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Close"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _unit_name = ""; + // Layer name widgets + _knot_x_entry.set_activates_default(true); + _knot_x_entry.set_digits(4); + _knot_x_entry.set_increments(1,1); + _knot_x_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_x_entry.set_hexpand(); + _knot_x_label.set_label(_("Position X:")); + _knot_x_label.set_halign(Gtk::ALIGN_END); + _knot_x_label.set_valign(Gtk::ALIGN_CENTER); + + _knot_y_entry.set_activates_default(true); + _knot_y_entry.set_digits(4); + _knot_y_entry.set_increments(1,1); + _knot_y_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_y_entry.set_hexpand(); + _knot_y_label.set_label(_("Position Y:")); + _knot_y_label.set_halign(Gtk::ALIGN_END); + _knot_y_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_knot_x_label, 0, 0, 1, 1); + _layout_table.attach(_knot_x_entry, 1, 0, 1, 1); + + _layout_table.attach(_knot_y_label, 0, 1, 1, 1); + _layout_table.attach(_knot_y_entry, 1, 1, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)), + true + ) + ); + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_knot_y_entry); +} + +KnotPropertiesDialog::~KnotPropertiesDialog() { + + _setDesktop(nullptr); +} + +void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name) +{ + KnotPropertiesDialog *dialog = new KnotPropertiesDialog(); + dialog->_setDesktop(desktop); + dialog->_setKnotPoint(pt->position(), unit_name); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Knot Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +KnotPropertiesDialog::_apply() +{ + double d_x = Inkscape::Util::Quantity::convert(_knot_x_entry.get_value(), _unit_name, "px"); + double d_y = Inkscape::Util::Quantity::convert(_knot_y_entry.get_value(), _unit_name, "px"); + _knotpoint->moveto(Geom::Point(d_x, d_y)); + _knotpoint->moved_signal.emit(_knotpoint, _knotpoint->position(), 0); + _close(); +} + +void +KnotPropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun(&::operator delete), this), + false + ) + ); +} + +bool KnotPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void KnotPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void KnotPropertiesDialog::_setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name) +{ + _unit_name = unit_name; + _knot_x_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.x(), "px", _unit_name)); + _knot_y_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.y(), "px", _unit_name)); + _knot_x_label.set_label(g_strdup_printf(_("Position X (%s):"), _unit_name.c_str())); + _knot_y_label.set_label(g_strdup_printf(_("Position Y (%s):"), _unit_name.c_str())); +} + +void KnotPropertiesDialog::_setPt(const SPKnot *pt) +{ + _knotpoint = const_cast(pt); +} + +void KnotPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + + +/* + 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 : diff --git a/src/ui/dialog/knot-properties.h b/src/ui/dialog/knot-properties.h new file mode 100644 index 0000000..fb88fad --- /dev/null +++ b/src/ui/dialog/knot-properties.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief + */ +/* Author: + * Bryce W. Harrington + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_KNOT_PROPERTIES_H +#define INKSCAPE_DIALOG_KNOT_PROPERTIES_H + +#include +#include +#include +#include +#include <2geom/point.h> +#include "knot.h" +#include "ui/tools/measure-tool.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +class KnotPropertiesDialog : public Gtk::Dialog { + public: + KnotPropertiesDialog(); + ~KnotPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name); + +protected: + + SPDesktop *_desktop; + SPKnot *_knotpoint; + + Gtk::Label _knot_x_label; + Gtk::SpinButton _knot_x_entry; + Gtk::Label _knot_y_label; + Gtk::SpinButton _knot_y_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + Glib::ustring _unit_name; + + sigc::connection _destroy_connection; + + static KnotPropertiesDialog &_instance() { + static KnotPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const SPKnot *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + friend class Inkscape::UI::Tools::MeasureTool; + +private: + KnotPropertiesDialog(KnotPropertiesDialog const &); // no copy + KnotPropertiesDialog &operator=(KnotPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + 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 : diff --git a/src/ui/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp new file mode 100644 index 0000000..083f104 --- /dev/null +++ b/src/ui/dialog/layer-properties.cpp @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington + * Andrius R. + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layer-properties.h" +#include +#include + +#include "inkscape.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "verbs.h" +#include "selection-chemistry.h" +#include "ui/icon-names.h" +#include "ui/widget/imagetoggler.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +LayerPropertiesDialog::LayerPropertiesDialog() + : _strategy(nullptr), + _desktop(nullptr), + _layer(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _layer_name_entry.set_activates_default(true); + _layer_name_label.set_label(_("Layer name:")); + _layer_name_label.set_halign(Gtk::ALIGN_START); + _layer_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_layer_name_label, 0, 0, 1, 1); + + _layer_name_entry.set_halign(Gtk::ALIGN_FILL); + _layer_name_entry.set_valign(Gtk::ALIGN_FILL); + _layer_name_entry.set_hexpand(); + _layout_table.attach(_layer_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &LayerPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &LayerPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &LayerPropertiesDialog::_close)), + true + ) + ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +LayerPropertiesDialog::~LayerPropertiesDialog() { + + _setDesktop(nullptr); + _setLayer(nullptr); +} + +void LayerPropertiesDialog::_showDialog(LayerPropertiesDialog::Strategy &strategy, + SPDesktop *desktop, SPObject *layer) +{ + LayerPropertiesDialog *dialog = new LayerPropertiesDialog(); + + dialog->_strategy = &strategy; + dialog->_setDesktop(desktop); + dialog->_setLayer(layer); + + dialog->_strategy->setup(*dialog); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +LayerPropertiesDialog::_apply() +{ + g_assert(_strategy != nullptr); + + _strategy->perform(*this); + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_NONE, + _("Add layer")); + + _close(); +} + +void +LayerPropertiesDialog::_close() +{ + _setLayer(nullptr); + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun(&::operator delete), this), + false + ) + ); +} + +void +LayerPropertiesDialog::_setup_position_controls() { + if ( nullptr == _layer || _desktop->currentRoot() == _layer ) { + // no layers yet, so option above/below/sublayer is useless + return; + } + + _position_visible = true; + _dropdown_list = Gtk::ListStore::create(_dropdown_columns); + _layer_position_combo.set_model(_dropdown_list); + _layer_position_combo.pack_start(_label_renderer); + _layer_position_combo.set_cell_data_func(_label_renderer, + sigc::mem_fun(*this, &LayerPropertiesDialog::_prepareLabelRenderer)); + + Gtk::ListStore::iterator row; + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_ABOVE); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Above current"))); + _layer_position_combo.set_active(row); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_BELOW); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Below current"))); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_CHILD); + row->set_value(_dropdown_columns.name, Glib::ustring(_("As sublayer of current"))); + + _layer_position_label.set_label(_("Position:")); + _layer_position_label.set_halign(Gtk::ALIGN_START); + _layer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layer_position_combo.set_halign(Gtk::ALIGN_FILL); + _layer_position_combo.set_valign(Gtk::ALIGN_FILL); + _layer_position_combo.set_hexpand(); + _layout_table.attach(_layer_position_combo, 1, 1, 1, 1); + + _layout_table.attach(_layer_position_label, 0, 1, 1, 1); + + show_all_children(); +} + +void +LayerPropertiesDialog::_setup_layers_controls() { + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + _store = Gtk::TreeStore::create( *zoop ); + _tree.set_model( _store ); + _tree.set_headers_visible(false); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + } + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + } + + Gtk::CellRendererText *_text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + Gtk::TreeView::Column *_name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleKeyEvent), false ); + _tree.signal_button_press_event().connect_notify( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleButtonEvent) ); + + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + _scroller.set_size_request(220, 180); + + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if ( root ) { + SPObject* target = _desktop->currentLayer(); + _store->clear(); + _addLayer( document, SP_OBJECT(root), nullptr, target, 0 ); + } + + _layout_table.remove(_layer_name_entry); + _layout_table.remove(_layer_name_label); + + _scroller.set_halign(Gtk::ALIGN_FILL); + _scroller.set_valign(Gtk::ALIGN_FILL); + _scroller.set_hexpand(); + _scroller.set_vexpand(); + _layout_table.attach(_scroller, 0, 1, 2, 1); + + show_all_children(); +} + +void LayerPropertiesDialog::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + int _maxNestDepth = 20; + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->id, child->label() ); +#endif // DUMP_LAYERS + + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = child; + row[_model->_colLabel] = child->label() ? child->label() : child->getId(); + row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; + + if ( target && child == target ) { + _tree.expand_to_path( _store->get_path(iter) ); + + Glib::RefPtr select = _tree.get_selection(); + select->select(iter); + + //_checkTreeSelection(); + } + + _addLayer( doc, child, &row, target, level + 1 ); + } + } + } +} + +SPObject* LayerPropertiesDialog::_selectedLayer() +{ + SPObject* obj = nullptr; + + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[_model->_colObject]; + } + + return obj; +} + +bool LayerPropertiesDialog::_handleKeyEvent(GdkEventKey *event) +{ + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _strategy->perform(*this); + _close(); + return true; + } + break; + } + return false; +} + +void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _strategy->perform(*this); + _close(); + } +} + +/** Formats the label for a given layer row + */ +void LayerPropertiesDialog::_prepareLabelRenderer( + Gtk::TreeModel::const_iterator const &row +) { + Glib::ustring name=(*row)[_dropdown_columns.name]; + _label_renderer.property_markup() = name.c_str(); +} + +void LayerPropertiesDialog::Rename::setup(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + dialog.set_title(_("Rename Layer")); + gchar const *name = desktop->currentLayer()->label(); + dialog._layer_name_entry.set_text(( name ? name : _("Layer") )); + dialog._apply_button.set_label(_("_Rename")); +} + +void LayerPropertiesDialog::Rename::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + desktop->layer_manager->renameLayer( desktop->currentLayer(), + (gchar *)name.c_str(), + FALSE + ); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, + _("Rename layer")); + // TRANSLATORS: This means "The layer has been renamed" + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer")); +} + +void LayerPropertiesDialog::Create::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Add Layer")); + + // Set the initial name to the "next available" layer name + LayerManager *mgr = dialog._desktop->layer_manager; + Glib::ustring newName = mgr->getNextLayerName(nullptr, dialog._desktop->currentLayer()->label()); + dialog._layer_name_entry.set_text(newName.c_str()); + dialog._apply_button.set_label(_("_Add")); + dialog._setup_position_controls(); +} + +void LayerPropertiesDialog::Create::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + + LayerRelativePosition position = LPOS_ABOVE; + + if (dialog._position_visible) { + Gtk::ListStore::iterator activeRow(dialog._layer_position_combo.get_active()); + position = activeRow->get_value(dialog._dropdown_columns.position); + } + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + + SPObject *new_layer=Inkscape::create_layer(desktop->currentRoot(), dialog._layer, position); + + if (!name.empty()) { + desktop->layer_manager->renameLayer( new_layer, (gchar *)name.c_str(), TRUE ); + } + desktop->getSelection()->clear(); + desktop->setCurrentLayer(new_layer); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created.")); +} + +void LayerPropertiesDialog::Move::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Move to Layer")); + //TODO: find an unused layer number, forming name from _("Layer ") + "%d" + dialog._layer_name_entry.set_text(_("Layer")); + dialog._apply_button.set_label(_("_Move")); + dialog._setup_layers_controls(); +} + +void LayerPropertiesDialog::Move::perform(LayerPropertiesDialog &dialog) { + + SPObject *moveto = dialog._selectedLayer(); + dialog._desktop->selection->toLayer(moveto); +} + +void LayerPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +void LayerPropertiesDialog::_setLayer(SPObject *layer) { + if (layer) { + sp_object_ref(layer, nullptr); + } + if (_layer) { + sp_object_unref(_layer, nullptr); + } + _layer = layer; +} + +} // namespace +} // namespace +} // namespace + + +/* + 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 : diff --git a/src/ui/dialog/layer-properties.h b/src/ui/dialog/layer-properties.h new file mode 100644 index 0000000..7cdd130 --- /dev/null +++ b/src/ui/dialog/layer-properties.h @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LAYER_PROPERTIES_H +#define INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "layer-fns.h" +#include "ui/widget/layer-selector.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class LayerPropertiesDialog : public Gtk::Dialog { + public: + LayerPropertiesDialog(); + ~LayerPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showRename(SPDesktop *desktop, SPObject *layer) { + _showDialog(Rename::instance(), desktop, layer); + } + static void showCreate(SPDesktop *desktop, SPObject *layer) { + _showDialog(Create::instance(), desktop, layer); + } + static void showMove(SPDesktop *desktop, SPObject *layer) { + _showDialog(Move::instance(), desktop, layer); + } + +protected: + struct Strategy { + virtual ~Strategy() = default; + virtual void setup(LayerPropertiesDialog &)=0; + virtual void perform(LayerPropertiesDialog &)=0; + }; + struct Rename : public Strategy { + static Rename &instance() { static Rename instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Create : public Strategy { + static Create &instance() { static Create instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Move : public Strategy { + static Move &instance() { static Move instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + + friend struct Rename; + friend struct Create; + friend struct Move; + + Strategy *_strategy; + SPDesktop *_desktop; + SPObject *_layer; + + class PositionDropdownColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn position; + Gtk::TreeModelColumn name; + + PositionDropdownColumns() { + add(position); add(name); + } + }; + + Gtk::Label _layer_name_label; + Gtk::Entry _layer_name_entry; + Gtk::Label _layer_position_label; + Gtk::ComboBox _layer_position_combo; + Gtk::Grid _layout_table; + + bool _position_visible; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colVisible; + Gtk::TreeModelColumn _colLocked; + }; + + Gtk::TreeView _tree; + ModelColumns* _model; + Glib::RefPtr _store; + Gtk::ScrolledWindow _scroller; + + + PositionDropdownColumns _dropdown_columns; + Gtk::CellRendererText _label_renderer; + Glib::RefPtr _dropdown_list; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static LayerPropertiesDialog &_instance() { + static LayerPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setLayer(SPObject *layer); + + static void _showDialog(Strategy &strategy, SPDesktop *desktop, SPObject *layer); + void _apply(); + void _close(); + + void _setup_position_controls(); + void _setup_layers_controls(); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + void _addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + SPObject* _selectedLayer(); + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + +private: + LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy + LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + 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 : diff --git a/src/ui/dialog/layers.cpp b/src/ui/dialog/layers.cpp new file mode 100644 index 0000000..a268426 --- /dev/null +++ b/src/ui/dialog/layers.cpp @@ -0,0 +1,1004 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for layers + * + * Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layers.h" + +#include +#include +#include + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-root.h" + +#include "svg/css-ostringstream.h" + +#include "ui/contextmenu.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/imagetoggler.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +LayersPanel& LayersPanel::getInstance() +{ + return *new LayersPanel(); +} + +enum { + COL_VISIBLE = 1, + COL_LOCKED +}; + +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + DRAGNDROP +}; + +class LayersPanel::InternalUIBounce +{ +public: + int _actionCode; + SPObject* _target; +}; + +void LayersPanel::_styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ) +{ + bool set = false; + + if ( iconName ) { + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + btn.set_relief(Gtk::RELIEF_NONE); + set = true; + } + + if ( desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + if ( !set && action && action->image ) { + GtkWidget *child = sp_get_icon_image(action->image, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + set = true; + } + + if ( action && action->tip ) { + btn.set_tooltip_text (action->tip); + } + } + } + + if ( !set && fallback ) { + btn.set_label( fallback ); + } +} + + +Gtk::MenuItem& LayersPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &LayersPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +void LayersPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if ( action ) { + sp_action_perform( action, nullptr ); +// } else { +// g_message("no action"); + } +// } else { +// g_message("no verb for %u", code); + } +// } else { +// g_message("no active desktop"); + } +} + +// SP_VERB_LAYER_NEXT, +// SP_VERB_LAYER_PREV, +void LayersPanel::_takeAction( int val ) +{ + if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_target = _selectedLayer(); + Glib::signal_timeout().connect( sigc::mem_fun(*this, &LayersPanel::_executeAction), 0 ); + } +} + +bool LayersPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _pending + && ( + (_pending->_actionCode == BUTTON_NEW || _pending->_actionCode == DRAGNDROP) + || !( (_desktop && _desktop->currentLayer()) + && (_desktop->currentLayer() != _pending->_target) + ) + ) + ) { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + break; + case BUTTON_BOTTOM: + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + break; + case BUTTON_UP: + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + break; + case BUTTON_DOWN: + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + break; + case BUTTON_DUPLICATE: + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + break; + case BUTTON_DELETE: + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +class LayersPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colVisible; + Gtk::TreeModelColumn _colLocked; +}; + +void LayersPanel::_updateLayer( SPObject *layer ) { + _store->foreach( sigc::bind(sigc::mem_fun(*this, &LayersPanel::_checkForUpdated), layer) ); +} + +bool LayersPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + /* + * We get notified of layer update here (from layer->setLabel()) before layer->label() is set + * with the correct value (sp-object bug?). So use the inkscape:label attribute instead which + * has the correct value (bug #168351) + */ + //row[_model->_colLabel] = layer->label() ? layer->label() : layer->defaultLabel(); + gchar const *label = layer->getAttribute("inkscape:label"); + row[_model->_colLabel] = label ? label : layer->defaultLabel(); + row[_model->_colVisible] = SP_IS_ITEM(layer) ? !SP_ITEM(layer)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false; + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_selectLayer( SPObject *layer ) { + if ( !layer || (_desktop && _desktop->doc() && (layer == _desktop->doc()->getRoot())) ) { + if ( _tree.get_selection()->count_selected_rows() != 0 ) { + _tree.get_selection()->unselect_all(); + } + } else { + _store->foreach( sigc::bind(sigc::mem_fun(*this, &LayersPanel::_checkForSelected), layer) ); + } + + _checkTreeSelection(); +} + +bool LayersPanel::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + _tree.expand_to_path( path ); + + Glib::RefPtr select = _tree.get_selection(); + + select->select(iter); + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_layersChanged() +{ +// g_message("_layersChanged()"); + if (_desktop) { + SPDocument* document = _desktop->doc(); + g_return_if_fail(document != nullptr); // bug #158: Crash on File>Quit + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); + if ( _desktop->layer_manager && _desktop->layer_manager->includes( root ) ) { + SPObject* target = _desktop->currentLayer(); + _store->clear(); + + #if DUMP_LAYERS + g_message("root:%p {%s} [%s]", root, root->id, root->label() ); + #endif // DUMP_LAYERS + _addLayer( document, root, nullptr, target, 0 ); + } + _selectedConnection.unblock(); + } + } +} + +void LayersPanel::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->getId(), child->label() ); +#endif // DUMP_LAYERS + + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = child; + row[_model->_colLabel] = child->defaultLabel(); + row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; + + if ( target && child == target ) { + _tree.expand_to_path( _store->get_path(iter) ); + + Glib::RefPtr select = _tree.get_selection(); + select->select(iter); + + _checkTreeSelection(); + } + + _addLayer( doc, child, &row, target, level + 1 ); + } + } + } +} + +SPObject* LayersPanel::_selectedLayer() +{ + SPObject* obj = nullptr; + + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[_model->_colObject]; + } + + return obj; +} + +void LayersPanel::_pushTreeSelectionToCurrent() +{ + // TODO hunt down the possible API abuse in getting NULL + if ( _desktop && _desktop->layer_manager && _desktop->currentRoot() ) { + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + SPObject* curr = _desktop->currentLayer(); + if ( curr != inTree ) { + _desktop->layer_manager->setCurrentLayer( inTree ); + } + } else { + _desktop->layer_manager->setCurrentLayer( _desktop->doc()->getRoot() ); + } + } +} + +void LayersPanel::_checkTreeSelection() +{ + bool sensitive = false; + bool sensitiveNonTop = false; + bool sensitiveNonBottom = false; + if ( _tree.get_selection()->count_selected_rows() > 0 ) { + sensitive = true; + + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + + sensitiveNonTop = (Inkscape::next_layer(inTree->parent, inTree) != nullptr); + sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != nullptr); + + } + } + + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } +} + +void LayersPanel::_preToggle( GdkEvent const *event ) +{ + + if ( _toggleEvent ) { + gdk_event_free(_toggleEvent); + _toggleEvent = nullptr; + } + + if ( event && (event->type == GDK_BUTTON_PRESS) ) { + // Make a copy so we can keep it around. + _toggleEvent = gdk_event_copy(const_cast(event)); + } +} + +void LayersPanel::_toggled( Glib::ustring const& str, int targetCol ) +{ + g_return_if_fail(_desktop != nullptr); + + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + Glib::ustring tmp = row[_model->_colLabel]; + + SPObject* obj = row[_model->_colObject]; + SPItem* item = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + if ( item ) { + switch ( targetCol ) { + case COL_VISIBLE: + { + bool newValue = !row[_model->_colVisible]; + row[_model->_colVisible] = newValue; + item->setHidden( !newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Unhide layer") : _("Hide layer")); + } + break; + + case COL_LOCKED: + { + bool newValue = !row[_model->_colLocked]; + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Lock layer") : _("Unlock layer")); + } + break; + } + } + Inkscape::SelectionHelper::fixSelection(_desktop); +} + +bool LayersPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + // TODO - fix to a better is-popup function + Gtk::TreeModel::Path path; + int x = static_cast(event->x); + int y = static_cast(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + + _popupMenu.popup_at_pointer(reinterpret_cast(event)); + } + } + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1) + && (event->state & GDK_MOD1_MASK)) { + // Alt left click on the visible/lock columns - eat this event to keep row selection + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1) || + col == _tree.get_column(COL_LOCKED-1)) { + return true; + } + } + } + + // TODO - ImageToggler doesn't seem to handle Shift/Alt clicks - so we deal with them here. + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1) + && (event->state & (GDK_SHIFT_MASK | GDK_MOD1_MASK))) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } + } + } + + + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + return false; +} + +/* + * Drap and drop within the tree + * Save the drag source and drop target SPObjects and if its a drag between layers or into (sublayer) a layer + */ +bool LayersPanel::_handleDragDrop(const Glib::RefPtr& /*context*/, int x, int y, guint /*time*/) +{ + int cell_x = 0, cell_y = 0; + Gtk::TreeModel::Path target_path; + Gtk::TreeView::Column *target_column; + SPObject *selected = _selectedLayer(); + + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source = ( selected && SP_IS_ITEM(selected) ) ? SP_ITEM(selected) : nullptr; + + if (_tree.get_path_at_pos (x, y, target_path, target_column, cell_x, cell_y)) { + // Are we before, inside or after the drop layer + Gdk::Rectangle rect; + _tree.get_background_area (target_path, *target_column, rect); + int cell_height = rect.get_height(); + _dnd_into = (cell_y > (int)(cell_height * 1/3) && cell_y <= (int)(cell_height * 2/3)); + if (cell_y > (int)(cell_height * 2/3)) { + Gtk::TreeModel::Path next_path = target_path; + next_path.next(); + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into parent + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level + _dnd_target = nullptr; + } + } + } + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + _dnd_target = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + } + } + + _takeAction(DRAGNDROP); + + return false; +} + +/* + * Move a layer in response to a drag & drop action + */ +void LayersPanel::_doTreeMove( ) +{ + if (_dnd_source && _dnd_source->getRepr() ) { + if(!_dnd_target){ + _dnd_source->doWriteTransform(_dnd_source->i2doc_affine() * _dnd_source->document->getRoot()->i2doc_affine().inverse()); + }else{ + SPItem* parent = _dnd_into ? _dnd_target : dynamic_cast(_dnd_target->parent); + if(parent){ + Geom::Affine move = _dnd_source->i2doc_affine() * parent->i2doc_affine().inverse(); + _dnd_source->doWriteTransform(move); + } + } + _dnd_source->moveTo(_dnd_target, _dnd_into); + _selectLayer(_dnd_source); + _dnd_source = nullptr; + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Move layer")); + } +} + + +void LayersPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameLayer(row, new_text); +} + +void LayersPanel::_renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop && _desktop->layer_manager) { + SPObject* obj = row[_model->_colObject]; + if ( obj ) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + _desktop->layer_manager->renameLayer( obj, name.c_str(), FALSE ); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename layer")); + } + + } + } +} + +bool LayersPanel::_rowSelectFunction( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Constructor + */ +LayersPanel::LayersPanel() : + UI::Widget::Panel("/dialogs/layers", SP_VERB_DIALOG_LAYERS), + deskTrack(), + _maxNestDepth(20), + _desktop(nullptr), + _model(nullptr), + _pending(nullptr), + _toggleEvent(nullptr), + _compositeSettings(SP_VERB_DIALOG_LAYERS, "layers", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::OPACITY | + UI::Widget::SimpleFilterModifier::BLUR), + desktopChangeConn() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _maxNestDepth = prefs->getIntLimited("/dialogs/layers/maxDepth", 20, 1, 1000); + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _tree.set_model( _store ); + _tree.set_headers_visible(false); + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + eyeRenderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_VISIBLE) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + } + + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + renderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_LOCKED) ); + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + } + + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(nameColNum + 1); + + _compositeSettings.setSubject(&_subject); + + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &LayersPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &LayersPanel::_rowSelectFunction) ); + + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &LayersPanel::_handleDragDrop), false); + + _text_renderer->property_editable() = true; + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &LayersPanel::_handleEdited) ); + + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), false ); + + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _watching.push_back( &_compositeSettings ); + + _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + _layersPage.pack_end(_compositeSettings, Gtk::PACK_SHRINK); + _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_NEW, INKSCAPE_ICON("list-add"), C_("Layers", "New") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_BOTTOM, INKSCAPE_ICON("go-bottom"), C_("Layers", "Bot") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_LOWER, INKSCAPE_ICON("go-down"), C_("Layers", "Dn") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_RAISE, INKSCAPE_ICON("go-up"), C_("Layers", "Up") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_TOP, INKSCAPE_ICON("go-top"), C_("Layers", "Top") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + +// btn = Gtk::manage( new Gtk::Button("Dup") ); +// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) ); +// _buttonsRow.add( *btn ); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_DELETE, INKSCAPE_ICON("list-remove"), _("X") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + + + // ------------------------------------------------------- + { + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_layers", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RAISE, (int)BUTTON_UP ) ); + _watchingNonBottom.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOWER, (int)BUTTON_DOWN ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + + + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + setDesktop( targetDesktop ); + + show_all_children(); + + // restorePanelPrefs(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LayersPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +LayersPanel::~LayersPanel() +{ + setDesktop(nullptr); + + _compositeSettings.setSubject(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + + +void LayersPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _layerChangedConnection.disconnect(); + _layerUpdatedConnection.disconnect(); + _changedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //setLabel( _desktop->doc()->name ); + + LayerManager *mgr = _desktop->layer_manager; + if ( mgr ) { + _layerChangedConnection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayersPanel::_selectLayer) ); + _layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayersPanel::_updateLayer) ); + _changedConnection = mgr->connectChanged( sigc::mem_fun(*this, &LayersPanel::_layersChanged) ); + } + + _layersChanged(); + } + } + deskTrack.setBase(desktop); +} + + + +} //namespace Dialogs +} //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 : diff --git a/src/ui/dialog/layers.h b/src/ui/dialog/layers.h new file mode 100644 index 0000000..8a66eef --- /dev/null +++ b/src/ui/dialog/layers.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for layer UI. + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LAYERS_PANEL_H +#define SEEN_LAYERS_PANEL_H + +#include +#include +#include +#include +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" + +class SPObject; + +namespace Inkscape { + +class LayerManager; + +namespace UI { +namespace Dialog { + + +/** + * A panel that displays layers. + */ +class LayersPanel : public UI::Widget::Panel +{ +public: + LayersPanel(); + ~LayersPanel() override; + + static LayersPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + +private: + class ModelColumns; + class InternalUIBounce; + + LayersPanel(LayersPanel const &) = delete; // no copy + LayersPanel &operator=(LayersPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ); + void _fireAction( unsigned int code ); + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _preToggle( GdkEvent const *event ); + void _toggled( Glib::ustring const& str, int targetCol ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + bool _handleDragDrop(const Glib::RefPtr& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + void _checkTreeSelection(); + + void _takeAction( int val ); + bool _executeAction(); + + bool _rowSelectFunction( Glib::RefPtr const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _updateLayer(SPObject *layer); + bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer); + + void _selectLayer(SPObject *layer); + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer); + + void _layersChanged(); + void _addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + + SPObject* _selectedLayer(); + + // Hooked to the layer manager: + sigc::connection _layerChangedConnection; + sigc::connection _layerUpdatedConnection; + sigc::connection _changedConnection; + sigc::connection _addedConnection; + sigc::connection _removedConnection; + + // Internal + sigc::connection _selectedConnection; + + DesktopTracker deskTrack; + int _maxNestDepth; + SPDesktop* _desktop; + ModelColumns* _model; + InternalUIBounce* _pending; + gboolean _dnd_into; + SPItem* _dnd_source; + SPItem* _dnd_target; + GdkEvent* _toggleEvent; + bool _show_contextmenu_icons; + + Glib::RefPtr _store; + std::vector _watching; + std::vector _watchingNonTop; + std::vector _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _layersPage; + + UI::Widget::StyleSubject::CurrentLayer _subject; + UI::Widget::ObjectCompositeSettings _compositeSettings; + sigc::connection desktopChangeConn; +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_LAYERS_PANEL_H + +/* + 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 : diff --git a/src/ui/dialog/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp new file mode 100644 index 0000000..8dbe30a --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.cpp @@ -0,0 +1,933 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-add.h" +#include "desktop.h" +#include "dialog.h" +#include "io/resource.h" +#include "live_effects/effect.h" +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "preferences.h" +#include +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +bool sp_has_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + return true; + } + return false; +} + +void sp_add_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + if (!sp_has_fav(effect)) { + prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";"); + } +} + +void sp_remove_fav(Glib::ustring effect) +{ + if (sp_has_fav(effect)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + effect += ";"; + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + favlist.erase(pos, effect.length()); + prefs->setString("/dialogs/livepatheffect/favs", favlist); + } + } +} + +bool LivePathEffectAdd::mouseover(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2); + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, cursor); + g_object_unref(cursor); + return true; +} + +bool LivePathEffectAdd::mouseout(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, nullptr); + hide_pop_description(evt); + return true; +} + +LivePathEffectAdd::LivePathEffectAdd() + : converter(Inkscape::LivePathEffect::LPETypeConverter) + , _applied(false) + , _showfavs(false) +{ + Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-add.glade"); + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + _builder->get_widget("LPEDialogSelector", _LPEDialogSelector); + _builder->get_widget("LPESelectorFlowBox", _LPESelectorFlowBox); + _builder->get_widget("LPESelectorEffectInfoPop", _LPESelectorEffectInfoPop); + _builder->get_widget("LPEFilter", _LPEFilter); + _builder->get_widget("LPEInfo", _LPEInfo); + _builder->get_widget("LPEExperimental", _LPEExperimental); + _builder->get_widget("LPEScrolled", _LPEScrolled); + _builder->get_widget("LPESelectorEffectEventFavShow", _LPESelectorEffectEventFavShow); + _builder->get_widget("LPESelectorEffectInfoEventBox", _LPESelectorEffectInfoEventBox); + _builder->get_widget("LPESelectorEffectRadioPackLess", _LPESelectorEffectRadioPackLess); + _builder->get_widget("LPESelectorEffectRadioPackMore", _LPESelectorEffectRadioPackMore); + _builder->get_widget("LPESelectorEffectRadioList", _LPESelectorEffectRadioList); + + _LPEFilter->signal_search_changed().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_search)); + _LPEDialogSelector->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK); + _LPESelectorFlowBox->signal_set_focus_child().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_focus)); + + gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-effect.glade"); + for (int i = 0; i < static_cast(converter._length); ++i) { + Glib::RefPtr builder_effect; + try { + builder_effect = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + const LivePathEffect::EnumEffectData *data = &converter.data(i); + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + LPESelectorEffect->signal_button_press_event().connect( + sigc::bind, const LivePathEffect::EnumEffectData *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + Gtk::EventBox *LPESelectorEffectEventExpander; + builder_effect->get_widget("LPESelectorEffectEventExpander", LPESelectorEffectEventExpander); + LPESelectorEffectEventExpander->signal_button_press_event().connect( + sigc::bind>(sigc::mem_fun(*this, &LivePathEffectAdd::expand), builder_effect)); + LPESelectorEffectEventExpander->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffectEventExpander->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + const Glib::ustring label = _(converter.get_label(data->id).c_str()); + const Glib::ustring untranslated_label = converter.get_untranslated_label(data->id); + if (untranslated_label == label) { + LPEName->set_text(label); + } else { + LPEName->set_markup((label + "\n" + untranslated_label + "").c_str()); + } + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + const Glib::ustring description = _(converter.get_description(data->id).c_str()); + LPEDescription->set_text(description); + Gtk::ToggleButton *LPEExperimentalToggle; + builder_effect->get_widget("LPEExperimentalToggle", LPEExperimentalToggle); + bool active = converter.get_experimental(data->id) ? true : false; + LPEExperimentalToggle->set_active(active); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + LPEIcon->set_from_icon_name(converter.get_icon(data->id), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + Gtk::EventBox *LPESelectorEffectEventInfo; + builder_effect->get_widget("LPESelectorEffectEventInfo", LPESelectorEffectEventInfo); + LPESelectorEffectEventInfo->signal_enter_notify_event().connect(sigc::bind>( + sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect)); + Gtk::EventBox *LPESelectorEffectEventFav; + builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav); + Gtk::Image *fav = dynamic_cast(LPESelectorEffectEventFav->get_child()); + if (sp_has_fav(LPEName->get_text())) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + LPESelectorEffectEventFav->signal_button_press_event().connect(sigc::bind>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + Gtk::Image *favtop = dynamic_cast(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(LPEName->get_text())) { + favtop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favtop->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventApply; + builder_effect->get_widget("LPESelectorEffectEventApply", LPESelectorEffectEventApply); + LPESelectorEffectEventApply->signal_button_press_event().connect( + sigc::bind, const LivePathEffect::EnumEffectData *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + Gtk::ButtonBox *LPESelectorButtonBox; + builder_effect->get_widget("LPESelectorButtonBox", LPESelectorButtonBox); + LPESelectorButtonBox->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + _LPESelectorFlowBox->insert(*LPESelectorEffect, i); + LPESelectorEffect->get_parent()->signal_key_press_event().connect( + sigc::bind, const LivePathEffect::EnumEffectData *>( + sigc::mem_fun(*this, &LivePathEffectAdd::on_press_enter), builder_effect, &converter.data(i))); + LPESelectorEffect->get_parent()->get_style_context()->add_class( + ("LPEIndex" + Glib::ustring::format(i)).c_str()); + } + _LPESelectorFlowBox->set_activate_on_single_click(false); + _visiblelpe = _LPESelectorFlowBox->get_children().size(); + _LPEInfo->set_visible(false); + _LPESelectorEffectRadioPackLess->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 0)); + _LPESelectorEffectRadioPackMore->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 1)); + _LPESelectorEffectRadioList->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 2)); + _LPESelectorEffectEventFavShow->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_button_press_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::show_fav_toggler)); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::hide_pop_description)); + _LPESelectorEffectInfoEventBox->signal_enter_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPEExperimental->property_active().signal_changed().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::reload_effect_list)); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + int width; + int height; + window->get_size(width, height); + _LPEDialogSelector->resize(std::min(width - 300, 1440), std::min(height - 300, 900)); + _LPEDialogSelector->set_transient_for(*window); + _LPESelectorFlowBox->set_focus_vadjustment(_LPEScrolled->get_vadjustment()); + _LPEDialogSelector->show_all_children(); + _lasteffect = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + switch (mode) { + case 0: + _LPESelectorEffectRadioPackLess->set_active(); + viewChanged(0); + break; + case 1: + _LPESelectorEffectRadioPackMore->set_active(); + viewChanged(1); + break; + default: + _LPESelectorEffectRadioList->set_active(); + viewChanged(2); + } + Gtk::Widget *widg = dynamic_cast(_LPEDialogSelector); + INKSCAPE.signal_change_theme.connect(sigc::bind(sigc::ptr_fun(sp_add_top_window_classes), widg)); + sp_add_top_window_classes(widg); +} +const LivePathEffect::EnumEffectData *LivePathEffectAdd::getActiveData() +{ + return instance()._to_add; +} + +void LivePathEffectAdd::viewChanged(gint mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool changed = false; + if (mode == 2 && !_LPEDialogSelector->get_style_context()->has_class("LPEList")) { + _LPEDialogSelector->get_style_context()->add_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(1); + changed = true; + } else if (mode == 1 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackMore")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } else if (mode == 0 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackLess")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } + prefs->setInt("/dialogs/livepatheffect/dialogmode", mode); + if (changed) { + _LPESelectorFlowBox->unset_sort_func(); + _LPESelectorFlowBox->set_sort_func(sigc::mem_fun(this, &LivePathEffectAdd::on_sort)); + std::vector selected = _LPESelectorFlowBox->get_selected_children(); + if (selected.size() == 1) { + _LPESelectorFlowBox->get_selected_children()[0]->grab_focus(); + } + } +} + +void LivePathEffectAdd::on_focus(Gtk::Widget *widget) +{ + Gtk::FlowBoxChild *child = dynamic_cast(widget); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (child && mode != 2) { + for (auto i : _LPESelectorFlowBox->get_children()) { + Gtk::FlowBoxChild *leitem = dynamic_cast(i); + Gtk::EventBox *eventbox = dynamic_cast(leitem->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast(eventbox->get_child()); + if (box) { + std::vector contents = box->get_children(); + Gtk::Box *actions = dynamic_cast(contents[5]); + if (actions) { + actions->set_visible(false); + } + Gtk::EventBox *expander = dynamic_cast(contents[4]); + if (expander) { + expander->set_visible(true); + } + } + } + } + Gtk::EventBox *eventbox = dynamic_cast(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast(eventbox->get_child()); + if (box) { + std::vector contents = box->get_children(); + Gtk::EventBox *expander = dynamic_cast(contents[4]); + if (expander) { + expander->set_visible(false); + } + } + } + + child->show_all_children(); + _LPESelectorFlowBox->select_child(*child); + } +} + +bool LivePathEffectAdd::pop_description(GdkEventCrossing *evt, Glib::RefPtr builder_effect) +{ + Gtk::Image *LPESelectorEffectInfo; + builder_effect->get_widget("LPESelectorEffectInfo", LPESelectorEffectInfo); + _LPESelectorEffectInfoPop->set_relative_to(*LPESelectorEffectInfo); + + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + + Gtk::Image *LPESelectorEffectInfoIcon; + _builder->get_widget("LPESelectorEffectInfoIcon", LPESelectorEffectInfoIcon); + LPESelectorEffectInfoIcon->set_from_icon_name(LPEIcon->get_icon_name(), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + + Gtk::Label *LPESelectorEffectInfoName; + _builder->get_widget("LPESelectorEffectInfoName", LPESelectorEffectInfoName); + LPESelectorEffectInfoName->set_text(LPEName->get_text()); + + Gtk::Label *LPESelectorEffectInfoDescription; + _builder->get_widget("LPESelectorEffectInfoDescription", LPESelectorEffectInfoDescription); + LPESelectorEffectInfoDescription->set_text(LPEDescription->get_text()); + + _LPESelectorEffectInfoPop->show(); + + return true; +} + +bool LivePathEffectAdd::hide_pop_description(GdkEventCrossing *evt) +{ + _LPESelectorEffectInfoPop->hide(); + return true; +} + +bool LivePathEffectAdd::fav_toggler(GdkEventButton *evt, Glib::RefPtr builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Image *LPESelectorEffectFav; + builder_effect->get_widget("LPESelectorEffectFav", LPESelectorEffectFav); + Gtk::Image *LPESelectorEffectFavTop; + builder_effect->get_widget("LPESelectorEffectFavTop", LPESelectorEffectFavTop); + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + if (LPESelectorEffectFav && LPESelectorEffectEventFavTop) { + if (sp_has_fav(LPEName->get_text())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + } else { + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + } + LPESelectorEffectFavTop->set_from_icon_name("draw-star-outline", + Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_remove_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + if (_showfavs) { + reload_effect_list(); + } + } else { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectFavTop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_add_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + } + } + return true; +} + +bool LivePathEffectAdd::show_fav_toggler(GdkEventButton *evt) +{ + _showfavs = !_showfavs; + Gtk::Image *favimage = dynamic_cast(_LPESelectorEffectEventFavShow->get_child()); + if (favimage) { + if (_showfavs) { + favimage->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favimage->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + } + reload_effect_list(); + return true; +} + +bool LivePathEffectAdd::apply(GdkEventButton *evt, Glib::RefPtr builder_effect, + const LivePathEffect::EnumEffectData *to_add) +{ + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast(LPESelectorEffect->get_parent()); + _LPESelectorFlowBox->select_child(*flowboxchild); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; +} + +bool LivePathEffectAdd::on_press_enter(GdkEventKey *key, Glib::RefPtr builder_effect, + const LivePathEffect::EnumEffectData *to_add) +{ + if (key->keyval == 65293 || key->keyval == 65421) { + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast(LPESelectorEffect->get_parent()); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; + } + return false; +} + +bool LivePathEffectAdd::expand(GdkEventButton *evt, Glib::RefPtr builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *child = dynamic_cast(LPESelectorEffect->get_parent()); + if (child) { + child->grab_focus(); + } + return true; +} + + + +bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child) +{ + std::vector classes = child->get_style_context()->list_classes(); + int pos = 0; + for (auto childclass : classes) { + size_t s = childclass.find("LPEIndex", 0); + if (s != -1) { + childclass = childclass.erase(0, 8); + pos = std::stoi(childclass); + } + } + const LivePathEffect::EnumEffectData *data = &converter.data(pos); + bool disable = false; + if (_item_type == "group" && !converter.get_on_group(data->id)) { + disable = true; + } else if (_item_type == "shape" && !converter.get_on_shape(data->id)) { + disable = true; + } else if (_item_type == "path" && !converter.get_on_path(data->id)) { + disable = true; + } + + if (!_has_clip && data->id == Inkscape::LivePathEffect::POWERCLIP) { + disable = true; + } + if (!_has_mask && data->id == Inkscape::LivePathEffect::POWERMASK) { + disable = true; + } + + if (disable) { + child->get_style_context()->add_class("lpedisabled"); + } else { + child->get_style_context()->remove_class("lpedisabled"); + } + child->set_valign(Gtk::ALIGN_START); + Gtk::EventBox *eventbox = dynamic_cast(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast(eventbox->get_child()); + if (box) { + std::vector contents = box->get_children(); + Gtk::Overlay *overlay = dynamic_cast(contents[0]); + std::vector content_overlay = overlay->get_children(); + + Gtk::Label *lpename = dynamic_cast(contents[1]); + if (!sp_has_fav(lpename->get_text()) && _showfavs) { + return false; + } + Gtk::ToggleButton *experimental = dynamic_cast(contents[3]); + if (experimental) { + if (experimental->get_active() && !_LPEExperimental->get_active()) { + return false; + } + } + Gtk::Label *lpedesc = dynamic_cast(contents[2]); + if (lpedesc) { + size_t s = lpedesc->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + if (_LPEFilter->get_text().length() < 1) { + _visiblelpe++; + return true; + } + if (lpename) { + size_t s = lpename->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + } + } + return false; +} + +void LivePathEffectAdd::reload_effect_list() +{ + /* if(_LPEExperimental->get_active()) { + _LPEExperimental->get_style_context()->add_class("active"); + } else { + _LPEExperimental->get_style_context()->remove_class("active"); + } */ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("You don't have any favorites yet, please disable the favorites star")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_text(_("This is your favorite effects")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } +} + +void LivePathEffectAdd::on_search() +{ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } + } +} + +int LivePathEffectAdd::on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2) +{ + Glib::ustring name1 = ""; + Glib::ustring name2 = ""; + Gtk::EventBox *eventbox = dynamic_cast(child1->get_child()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast(contents[1]); + name1 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child1->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child1->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast(contents[0]); + if (overlay) { + std::vector contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(40); + icon->set_margin_right(25); + overlay->set_margin_right(5); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(0); + overlay->set_margin_right(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast(contents_overlay[1]); + if (LPESelectorEffectEventFavTop) { + Gtk::Image *fav = dynamic_cast(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child1->get_style_context()->add_class("lpefav"); + child1->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child1->get_style_context()->remove_class("lpefav"); + child1->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child1->get_style_context()->add_class("lpe"); + } + } + } + } + eventbox = dynamic_cast(child2->get_child()); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast(contents[1]); + name2 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child2->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child2->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast(contents[0]); + if (overlay) { + std::vector contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(33); + icon->set_margin_right(40); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast(contents_overlay[1]); + Gtk::Image *fav = dynamic_cast(LPESelectorEffectEventFavTop->get_child()); + if (LPESelectorEffectEventFavTop) { + if (sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child2->get_style_context()->add_class("lpefav"); + child2->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child2->get_style_context()->remove_class("lpefav"); + child2->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child2->get_style_context()->add_class("lpe"); + } + } + } + } + std::vector effect; + effect.push_back(name1); + effect.push_back(name2); + sort(effect.begin(), effect.end()); + /* if (sp_has_fav(name1) && sp_has_fav(name2)) { + return effect[0] == name1?-1:1; + } + if (sp_has_fav(name1)) { + return -1; + } */ + if (effect[0] == name1) { //&& !sp_has_fav(name2)) { + return -1; + } + return 1; +} + + +void LivePathEffectAdd::onClose() { _LPEDialogSelector->hide(); } + +void LivePathEffectAdd::onKeyEvent(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_Escape) { + onClose(); + } +} + +void LivePathEffectAdd::show(SPDesktop *desktop) +{ + LivePathEffectAdd &dial = instance(); + Inkscape::Selection *sel = desktop->getSelection(); + if (sel && !sel->isEmpty()) { + SPItem *item = sel->singleItem(); + if (item) { + SPShape *shape = dynamic_cast(item); + SPPath *path = dynamic_cast(item); + SPGroup *group = dynamic_cast(item); + dial._has_clip = (item->getClipObject() != nullptr); + dial._has_mask = (item->getMaskObject() != nullptr); + dial._item_type = ""; + if (group) { + dial._item_type = "group"; + } else if (path) { + dial._item_type = "path"; + } else if (shape) { + dial._item_type = "shape"; + } else { + dial._LPEDialogSelector->hide(); + return; + } + } + } + dial._applied = false; + dial._LPESelectorFlowBox->unset_sort_func(); + dial._LPESelectorFlowBox->unset_filter_func(); + dial._LPESelectorFlowBox->set_filter_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_filter)); + dial._LPESelectorFlowBox->set_sort_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_sort)); + Glib::RefPtr vadjust = dial._LPEScrolled->get_vadjustment(); + vadjust->set_value(vadjust->get_lower()); + dial._LPEDialogSelector->show(); + int searchlen = dial._LPEFilter->get_text().length(); + if (searchlen > 0) { + dial._LPEFilter->select_region (0, searchlen); + dial._LPESelectorFlowBox->unselect_all(); + } else if (dial._lasteffect) { + dial._lasteffect->grab_focus(); + } + dial._LPEDialogSelector->run(); + dial._LPEDialogSelector->hide(); +} + +} // 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 : diff --git a/src/ui/dialog/livepatheffect-add.h b/src/ui/dialog/livepatheffect-add.h new file mode 100644 index 0000000..bd8dca4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H +#define INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H + +#include "live_effects/effect-enum.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to list the live path effects that can be added + * + */ +class LivePathEffectAdd { + public: + LivePathEffectAdd(); + ~LivePathEffectAdd() = default; + ; + + /** + * Show the dialog + */ + static void show(SPDesktop *desktop); + /** + * Returns true is the "Add" button was pressed + */ + static bool isApplied() { return instance()._applied; } + + static const LivePathEffect::EnumEffectData *getActiveData(); + + protected: + /** + * Close button was clicked + */ + void onClose(); + bool on_filter(Gtk::FlowBoxChild *child); + int on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2); + void on_search(); + void on_focus(Gtk::Widget *widg); + bool pop_description(GdkEventCrossing *evt, Glib::RefPtr builder_effect); + bool hide_pop_description(GdkEventCrossing *evt); + bool fav_toggler(GdkEventButton *evt, Glib::RefPtr builder_effect); + bool apply(GdkEventButton *evt, Glib::RefPtr builder_effect, + const LivePathEffect::EnumEffectData *to_add); + bool on_press_enter(GdkEventKey *key, Glib::RefPtr builder_effect, + const LivePathEffect::EnumEffectData *to_add); + bool expand(GdkEventButton *evt, Glib::RefPtr builder_effect); + bool show_fav_toggler(GdkEventButton *evt); + void viewChanged(gint mode); + bool mouseover(GdkEventCrossing *evt, GtkWidget *wdg); + bool mouseout(GdkEventCrossing *evt, GtkWidget *wdg); + void reload_effect_list(); + /** + * Add button was clicked + */ + void onAdd(); + /** + * Tree was clicked + */ + void onButtonEvent(GdkEventButton* evt); + + /** + * Key event + */ + void onKeyEvent(GdkEventKey* evt); +private: + Gtk::Button _add_button; + Gtk::Button _close_button; + Gtk::Dialog *_LPEDialogSelector; + Glib::RefPtr _builder; + Gtk::FlowBox *_LPESelectorFlowBox; + Gtk::Popover *_LPESelectorEffectInfoPop; + Gtk::EventBox *_LPESelectorEffectEventFavShow; + Gtk::EventBox *_LPESelectorEffectInfoEventBox; + Gtk::RadioButton *_LPESelectorEffectRadioList; + Gtk::RadioButton *_LPESelectorEffectRadioPackLess; + Gtk::RadioButton *_LPESelectorEffectRadioPackMore; + Gtk::Switch *_LPEExperimental; + Gtk::SearchEntry *_LPEFilter; + Gtk::ScrolledWindow *_LPEScrolled; + Gtk::Label *_LPEInfo; + Gtk::Box *_LPESelector; + guint _visiblelpe; + Glib::ustring _item_type; + bool _has_clip; + bool _has_mask; + Gtk::FlowBoxChild *_lasteffect; + const LivePathEffect::EnumEffectData *_to_add; + bool _showfavs; + bool _applied; + class Effect; + const LivePathEffect::EnumEffectDataConverter &converter; + static LivePathEffectAdd &instance() + { + static LivePathEffectAdd instance_; + return instance_; + } + LivePathEffectAdd(LivePathEffectAdd const &) = delete; // no copy + LivePathEffectAdd &operator=(LivePathEffectAdd const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H + +/* + 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 : diff --git a/src/ui/dialog/livepatheffect-editor.cpp b/src/ui/dialog/livepatheffect-editor.cpp new file mode 100644 index 0000000..472f9ac --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.cpp @@ -0,0 +1,634 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Live Path Effect editing dialog - implementation. + */ +/* Authors: + * Johan Engelen + * Steren Giannini + * Bastien Bouclet + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-editor.h" + +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "livepatheffect-add.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" + +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-use.h" +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/imagetoggler.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/*#################### + * Callback functions + */ + + +void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data) +{ + LivePathEffectEditor *lpeeditor = static_cast(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->onSelectionChanged(selection); + lpeeditor->_on_button_release(nullptr); //to force update widgets +} + +void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data) +{ + + LivePathEffectEditor *lpeeditor = static_cast(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->onSelectionChanged(selection); +} + +static void lpe_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + + +/* + * LivePathEffectEditor + * + * TRANSLATORS: this dialog is accessible via menu Path - Path Effect Editor... + * + */ + +LivePathEffectEditor::LivePathEffectEditor() + : UI::Widget::Panel("/dialogs/livepatheffect", SP_VERB_DIALOG_LIVE_PATH_EFFECT), + deskTrack(), + lpe_list_locked(false), + effectwidget(nullptr), + status_label("", Gtk::ALIGN_CENTER), + effectcontrol_frame(""), + button_add(), + button_remove(), + button_up(), + button_down(), + current_desktop(nullptr), + current_lpeitem(nullptr), + current_lperef(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(4); + + //Add the TreeView, inside a ScrolledWindow, with the button underneath: + scrolled_window.add(effectlist_view); + scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrolled_window.set_shadow_type(Gtk::SHADOW_IN); + scrolled_window.set_size_request(210, 70); + + effectapplication_hbox.set_spacing(4); + effectcontrol_vbox.set_spacing(4); + + effectlist_vbox.pack_start(scrolled_window, Gtk::PACK_EXPAND_WIDGET); + effectlist_vbox.pack_end(toolbar_hbox, Gtk::PACK_SHRINK); + effectcontrol_eventbox.add_events(Gdk::BUTTON_RELEASE_MASK); + effectcontrol_eventbox.signal_button_release_event().connect(sigc::mem_fun(*this, &LivePathEffectEditor::_on_button_release) ); + effectcontrol_eventbox.add(effectcontrol_vbox); + effectcontrol_frame.add(effectcontrol_eventbox); + + button_add.set_tooltip_text(_("Add path effect")); + lpe_style_button(button_add, INKSCAPE_ICON("list-add")); + button_add.set_relief(Gtk::RELIEF_NONE); + + button_remove.set_tooltip_text(_("Delete current path effect")); + lpe_style_button(button_remove, INKSCAPE_ICON("list-remove")); + button_remove.set_relief(Gtk::RELIEF_NONE); + + button_up.set_tooltip_text(_("Raise the current path effect")); + lpe_style_button(button_up, INKSCAPE_ICON("go-up")); + button_up.set_relief(Gtk::RELIEF_NONE); + + button_down.set_tooltip_text(_("Lower the current path effect")); + lpe_style_button(button_down, INKSCAPE_ICON("go-down")); + button_down.set_relief(Gtk::RELIEF_NONE); + + // Add toolbar items to toolbar + toolbar_hbox.set_layout (Gtk::BUTTONBOX_END); + toolbar_hbox.add( button_add ); + toolbar_hbox.set_child_secondary( button_add , true); + toolbar_hbox.add( button_remove ); + toolbar_hbox.set_child_secondary( button_remove , true); + toolbar_hbox.add( button_up ); + toolbar_hbox.add( button_down ); + + //Create the Tree model: + effectlist_store = Gtk::ListStore::create(columns); + effectlist_view.set_model(effectlist_store); + effectlist_view.set_headers_visible(false); + + // Handle tree selections + effectlist_selection = effectlist_view.get_selection(); + effectlist_selection->signal_changed().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_effect_selection_changed) ); + + //Add the visibility icon column: + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = effectlist_view.append_column("is_visible", *eyeRenderer) - 1; + eyeRenderer->signal_toggled().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_visibility_toggled) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = effectlist_view.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), columns.col_visible ); + } + + //Add the effect name column: + effectlist_view.append_column("Effect", columns.col_name); + + contents->pack_start(effectlist_vbox, true, true); + contents->pack_start(status_label, false, false); + contents->pack_start(effectcontrol_frame, false, false); + + effectcontrol_frame.hide(); + + // connect callback functions to buttons + button_add.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAdd)); + button_remove.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onRemove)); + button_up.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onUp)); + button_down.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onDown)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LivePathEffectEditor::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); +} + +LivePathEffectEditor::~LivePathEffectEditor() +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } +} + +bool LivePathEffectEditor::_on_button_release(GdkEventButton* button_event) { + Glib::RefPtr sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + return true; + } + Gtk::TreeModel::iterator it = sel->get_selected(); + LivePathEffect::LPEObjectReference * lperef = (*it)[columns.lperef]; + if (lperef && current_lpeitem && current_lperef != lperef) { + if (lperef->getObject()) { + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + } + } + } + return true; +} + +void +LivePathEffectEditor::showParams(LivePathEffect::Effect& effect) +{ + if (effectwidget && !effect.refresh_widgets) { + return; + } + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + effectwidget = effect.newWidget(); + effectcontrol_frame.set_label(effect.getName()); + effectcontrol_vbox.pack_start(*effectwidget, true, true); + + button_remove.show(); + status_label.hide(); + effectcontrol_frame.show(); + effectcontrol_vbox.show_all_children(); + // fixme: add resizing of dialog + effect.refresh_widgets = false; +} + +void +LivePathEffectEditor::selectInList(LivePathEffect::Effect* effect) +{ + Gtk::TreeNodeChildren chi = effectlist_view.get_model()->children(); + for (Gtk::TreeIter ci = chi.begin() ; ci != chi.end(); ci++) { + if (ci->get_value(columns.lperef)->lpeobject->get_lpe() == effect && effectlist_view.get_selection()) + effectlist_view.get_selection()->select(ci); + } +} + + +void +LivePathEffectEditor::showText(Glib::ustring const &str) +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + status_label.show(); + status_label.set_label(str); + + effectcontrol_frame.hide(); + + // fixme: do resizing of dialog ? +} + +void +LivePathEffectEditor::set_sensitize_all(bool sensitive) +{ + //combo_effecttype.set_sensitive(sensitive); + button_add.set_sensitive(sensitive); + button_remove.set_sensitive(sensitive); + effectlist_view.set_sensitive(sensitive); + button_up.set_sensitive(sensitive); + button_down.set_sensitive(sensitive); +} + +void +LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel) +{ + if (lpe_list_locked) { + // this was triggered by selecting a row in the list, so skip reloading + lpe_list_locked = false; + return; + } + current_lpeitem = nullptr; + effectlist_store->clear(); + + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if ( item ) { + SPLPEItem *lpeitem = dynamic_cast(item); + if ( lpeitem ) { + effect_list_reload(lpeitem); + current_lpeitem = lpeitem; + set_sensitize_all(true); + if ( lpeitem->hasPathEffect() ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } else { + showText(_("Unknown effect is applied")); + } + } else { + showText(_("Click button to add an effect")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } + } else { + SPUse *use = dynamic_cast(item); + if ( use ) { + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *orig = use->get_original(); + if ( dynamic_cast(orig) || + dynamic_cast(orig) || + dynamic_cast(orig) ) + { + // Note that an SP_USE cannot have an LPE applied, so we only need to worry about the "add effect" case. + set_sensitize_all(true); + showText(_("Click add button to convert clone")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } + } else { + showText(_("Only one item can be selected")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } +} + +/* + * First clears the effectlist_store, then appends all effects from the effectlist. + */ +void +LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem) +{ + effectlist_store->clear(); + + PathEffectList effectlist = lpeitem->getEffectList(); + PathEffectList::iterator it; + for( it = effectlist.begin() ; it!=effectlist.end(); ++it) + { + if ( !(*it)->lpeobject ) { + continue; + } + + if ((*it)->lpeobject->get_lpe()) { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = (*it)->lpeobject->get_lpe()->getName(); + row[columns.lperef] = *it; + row[columns.col_visible] = (*it)->lpeobject->get_lpe()->isVisible(); + } else { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = _("Unknown effect"); + row[columns.lperef] = *it; + row[columns.col_visible] = false; + } + } +} + + +void +LivePathEffectEditor::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + + if ( desktop == current_desktop ) { + return; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } + + lpe_list_locked = false; + current_desktop = desktop; + if (desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + selection_changed_connection = selection->connectChanged( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_changed), this ) ); + selection_modified_connection = selection->connectModified( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_modified), this ) ); + + onSelectionChanged(selection); + } else { + onSelectionChanged(nullptr); + } +} + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +// TODO: factor out the effect applying code which can be called from anywhere. (selection-chemistry.cpp also needs it) +void +LivePathEffectEditor::onAdd() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + if ( dynamic_cast(item) ) { + // show effectlist dialog + using Inkscape::UI::Dialog::LivePathEffectAdd; + LivePathEffectAdd::show(current_desktop); + if ( !LivePathEffectAdd::isApplied()) { + return; + } + + SPDocument *doc = current_desktop->doc(); + + const LivePathEffect::EnumEffectData *data = + LivePathEffectAdd::getActiveData(); + if (!data) { + return; + } + item = sel->singleItem(); // get new item + + LivePathEffect::Effect::createAndApply(data->key.c_str(), doc, item); + + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } else { + SPUse *use = dynamic_cast(item); + if ( use ) { + // item is a clone. do not show effectlist dialog. + // convert to path, apply CLONE_ORIGINAL LPE, link it to the cloned path + + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *orig = use->get_original(); + if ( dynamic_cast(orig) || + dynamic_cast(orig) || + dynamic_cast(orig) ) + { + // select original + sel->set(orig); + + // delete clone but remember its id and transform + gchar *id = g_strdup(item->getRepr()->attribute("id")); + gchar *transform = g_strdup(item->getRepr()->attribute("transform")); + item->deleteObject(false); + item = nullptr; + + // run sp_selection_clone_original_path_lpe + sel->cloneOriginalPathLPE(true); + + SPItem *new_item = sel->singleItem(); + // Check that the cloning was successful. We don't want to change the ID of the original referenced path! + if (new_item && (new_item != orig)) { + new_item->setAttribute("id", id); + new_item->setAttribute("transform", transform); + } + g_free(id); + g_free(transform); + + /// \todo Add the LPE stack of the original path? + + SPDocument *doc = current_desktop->doc(); + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply Clone original path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + } + } + } +} + +void +LivePathEffectEditor::onRemove() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast(item); + if ( lpeitem ) { + sp_lpe_item_update_patheffect(lpeitem, false, false); + lpeitem->removeCurrentPathEffect(false); + current_lperef = nullptr; + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Remove path effect") ); + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + +} + +void LivePathEffectEditor::onUp() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->upCurrentPathEffect(); + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect up") ); + + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::onDown() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->downCurrentPathEffect(); + + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect down") ); + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::on_effect_selection_changed() +{ + Glib::RefPtr sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + button_remove.set_sensitive(false); + return; + } + button_remove.set_sensitive(true); + Gtk::TreeModel::iterator it = sel->get_selected(); + LivePathEffect::LPEObjectReference * lperef = (*it)[columns.lperef]; + + if (lperef && current_lpeitem && current_lperef != lperef) { + //The last condition ignore Gtk::TreeModel may occasionally be changed emitted when nothing has happened + if (lperef->getObject()) { + lpe_list_locked = true; // prevent reload of the list which would lose selection + current_lpeitem->setCurrentPathEffect(lperef); + current_lperef = lperef; + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + //To reload knots and helper paths + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + sel->clear(); + sel->add(item); + Inkscape::UI::Tools::sp_update_helperpath(); + } + } + } + } + } +} + +void LivePathEffectEditor::on_visibility_toggled( Glib::ustring const& str ) +{ + + Gtk::TreeModel::Children::iterator iter = effectlist_view.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + LivePathEffect::LPEObjectReference * lpeobjref = row[columns.lperef]; + + if ( lpeobjref && lpeobjref->lpeobject->get_lpe() ) { + bool newValue = !row[columns.col_visible]; + row[columns.col_visible] = newValue; + /* FIXME: this explicit writing to SVG is wrong. The lpe_item should have a method to disable/enable an effect within its stack. + * So one can call: lpe_item->setActive(lpeobjref->lpeobject); */ + lpeobjref->lpeobject->get_lpe()->getRepr()->setAttribute("is_visible", newValue ? "true" : "false"); + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast(item); + if ( lpeitem ) { + lpeobjref->lpeobject->get_lpe()->doOnVisibilityToggled(lpeitem); + } + } + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + newValue ? _("Activate path effect") : _("Deactivate path effect")); + } +} + +} // 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 : diff --git a/src/ui/dialog/livepatheffect-editor.h b/src/ui/dialog/livepatheffect-editor.h new file mode 100644 index 0000000..30524e4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Live Path Effect editing dialog + */ +/* Author: + * Johan Engelen + * + * Copyright (C) 2007 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H +#define INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H + +#include "ui/widget/panel.h" + +#include +#include +#include "ui/widget/combo-enums.h" +#include "ui/widget/frame.h" +#include "object/sp-item.h" +#include "live_effects/effect-enum.h" +#include +#include +#include +#include +#include +#include +#include "ui/dialog/desktop-tracker.h" + +class SPDesktop; +class SPLPEItem; + +namespace Inkscape { + +namespace LivePathEffect { + class Effect; + class LPEObjectReference; +} + +namespace UI { +namespace Dialog { + +class LivePathEffectEditor : public UI::Widget::Panel { +public: + LivePathEffectEditor(); + ~LivePathEffectEditor() override; + + static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); } + + void onSelectionChanged(Inkscape::Selection *sel); + void onSelectionModified(Inkscape::Selection *sel); + virtual void on_effect_selection_changed(); + void setDesktop(SPDesktop *desktop) override; + +private: + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection selection_changed_connection; + sigc::connection selection_modified_connection; + + // void add_entry(const char* name ); + void effect_list_reload(SPLPEItem *lpeitem); + + void set_sensitize_all(bool sensitive); + void showParams(LivePathEffect::Effect& effect); + void showText(Glib::ustring const &str); + void selectInList(LivePathEffect::Effect* effect); + + // callback methods for buttons on grids page. + void onAdd(); + void onRemove(); + void onUp(); + void onDown(); + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ModelColumns() + { + add(col_name); + add(lperef); + add(col_visible); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn col_name; + Gtk::TreeModelColumn lperef; + Gtk::TreeModelColumn col_visible; + }; + + bool lpe_list_locked; + //Inkscape::UI::Widget::ComboBoxEnum combo_effecttype; + + Gtk::Widget * effectwidget; + Gtk::Label status_label; + UI::Widget::Frame effectcontrol_frame; + Gtk::HBox effectapplication_hbox; + Gtk::VBox effectcontrol_vbox; + Gtk::EventBox effectcontrol_eventbox; + Gtk::VBox effectlist_vbox; + ModelColumns columns; + Gtk::ScrolledWindow scrolled_window; + Gtk::TreeView effectlist_view; + Glib::RefPtr effectlist_store; + Glib::RefPtr effectlist_selection; + + void on_visibility_toggled( Glib::ustring const& str); + bool _on_button_release(GdkEventButton* button_event); + Gtk::ButtonBox toolbar_hbox; + Gtk::Button button_add; + Gtk::Button button_remove; + Gtk::Button button_up; + Gtk::Button button_down; + + SPDesktop * current_desktop; + + SPLPEItem * current_lpeitem; + + LivePathEffect::LPEObjectReference * current_lperef; + + friend void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data); + friend void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data); + + LivePathEffectEditor(LivePathEffectEditor const &d); + LivePathEffectEditor& operator=(LivePathEffectEditor const &d); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H + +/* + 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 : diff --git a/src/ui/dialog/lpe-fillet-chamfer-properties.cpp b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp new file mode 100644 index 0000000..bbf4b31 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "lpe-fillet-chamfer-properties.h" +#include +#include +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +FilletChamferPropertiesDialog::FilletChamferPropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + mainVBox->set_homogeneous(false); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _fillet_chamfer_position_numeric.set_digits(4); + _fillet_chamfer_position_numeric.set_increments(1,1); + //todo: get tha max aloable infinity freeze the widget + _fillet_chamfer_position_numeric.set_range(0., SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_position_numeric.set_hexpand(); + _fillet_chamfer_position_label.set_label(_("Radius (pixels):")); + _fillet_chamfer_position_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_position_label, 0, 0, 1, 1); + _layout_table.attach(_fillet_chamfer_position_numeric, 1, 0, 1, 1); + _fillet_chamfer_chamfer_subdivisions.set_digits(0); + _fillet_chamfer_chamfer_subdivisions.set_increments(1,1); + //todo: get tha max aloable infinity freeze the widget + _fillet_chamfer_chamfer_subdivisions.set_range(0, SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_chamfer_subdivisions.set_hexpand(); + _fillet_chamfer_chamfer_subdivisions_label.set_label(_("Chamfer subdivisions:")); + _fillet_chamfer_chamfer_subdivisions_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_chamfer_subdivisions_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions_label, 0, 1, 1, 1); + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions, 1, 1, 1, 1); + _fillet_chamfer_type_fillet.set_label(_("Fillet")); + _fillet_chamfer_type_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_fillet.set_label(_("Inverse fillet")); + _fillet_chamfer_type_inverse_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_chamfer.set_label(_("Chamfer")); + _fillet_chamfer_type_chamfer.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_chamfer.set_label(_("Inverse chamfer")); + _fillet_chamfer_type_inverse_chamfer.set_group(_fillet_chamfer_type_group); + + + mainVBox->pack_start(_layout_table, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_chamfer, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_chamfer, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_apply)); + + signal_delete_event().connect(sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)), + true)); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_fillet_chamfer_position_numeric); +} + +FilletChamferPropertiesDialog::~FilletChamferPropertiesDialog() +{ + + _setDesktop(nullptr); +} + +void FilletChamferPropertiesDialog::showDialog( + SPDesktop *desktop, + double _amount, + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt, + bool _use_distance, + bool _aprox_radius, + Satellite _satellite) +{ + FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog(); + + dialog->_setDesktop(desktop); + dialog->_setUseDistance(_use_distance); + dialog->_setAprox(_aprox_radius); + dialog->_setAmount(_amount); + dialog->_setSatellite(_satellite); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Fillet-Chamfer")); + dialog->_apply_button.set_label(_("_Modify")); + + dialog->set_modal(true); + desktop->setWindowTransient(dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void FilletChamferPropertiesDialog::_apply() +{ + + double d_pos = _fillet_chamfer_position_numeric.get_value(); + if (d_pos >= 0) { + if (_fillet_chamfer_type_fillet.get_active() == true) { + _satellite.satellite_type = FILLET; + } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) { + _satellite.satellite_type = INVERSE_FILLET; + } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) { + _satellite.satellite_type = INVERSE_CHAMFER; + } else { + _satellite.satellite_type = CHAMFER; + } + if (_flexible) { + if (d_pos > 99.99999 || d_pos < 0) { + d_pos = 0; + } + d_pos = d_pos / 100; + } + _satellite.amount = d_pos; + size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value(); + if (steps < 1) { + steps = 1; + } + _satellite.steps = steps; + _knotpoint->knot_set_offset(_satellite); + } + _close(); +} + +void FilletChamferPropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun(&::operator delete), this), + false + ) + ); +} + +bool FilletChamferPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + return false; +} + +void FilletChamferPropertiesDialog::_handleButtonEvent(GdkEventButton *event) +{ + if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) { + _apply(); + } +} + +void FilletChamferPropertiesDialog::_setSatellite(Satellite satellite) +{ + double position; + std::string distance_or_radius = std::string(_("Radius")); + if (_aprox) { + distance_or_radius = std::string(_("Radius approximated")); + } + if (_use_distance) { + distance_or_radius = std::string(_("Knot distance")); + } + if (satellite.is_time) { + position = _amount * 100; + _flexible = true; + _fillet_chamfer_position_label.set_label(_("Position (%):")); + } else { + _flexible = false; + std::string posConcat = Glib::ustring::compose (_("%1:"), distance_or_radius); + _fillet_chamfer_position_label.set_label(_(posConcat.c_str())); + position = _amount; + } + _fillet_chamfer_position_numeric.set_value(position); + _fillet_chamfer_chamfer_subdivisions.set_value(satellite.steps); + if (satellite.satellite_type == FILLET) { + _fillet_chamfer_type_fillet.set_active(true); + } else if (satellite.satellite_type == INVERSE_FILLET) { + _fillet_chamfer_type_inverse_fillet.set_active(true); + } else if (satellite.satellite_type == CHAMFER) { + _fillet_chamfer_type_chamfer.set_active(true); + } else if (satellite.satellite_type == INVERSE_CHAMFER) { + _fillet_chamfer_type_inverse_chamfer.set_active(true); + } + _satellite = satellite; +} + +void FilletChamferPropertiesDialog::_setPt( + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt) +{ + _knotpoint = const_cast< + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *>( + pt); +} + + +void FilletChamferPropertiesDialog::_setAmount(double amount) +{ + _amount = amount; +} + + + +void FilletChamferPropertiesDialog::_setUseDistance(bool use_knot_distance) +{ + _use_distance = use_knot_distance; +} + +void FilletChamferPropertiesDialog::_setAprox(bool _aprox_radius) +{ + _aprox = _aprox_radius; +} + +void FilletChamferPropertiesDialog::_setDesktop(SPDesktop *desktop) +{ + if (desktop) { + Inkscape::GC::anchor(desktop); + } + if (_desktop) { + Inkscape::GC::release(_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + +/* + 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 diff --git a/src/ui/dialog/lpe-fillet-chamfer-properties.h b/src/ui/dialog/lpe-fillet-chamfer-properties.h new file mode 100644 index 0000000..26a0569 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H +#define INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H + +#include <2geom/point.h> +#include +#include "live_effects/parameter/satellitesarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class FilletChamferPropertiesDialog : public Gtk::Dialog { +public: + FilletChamferPropertiesDialog(); + ~FilletChamferPropertiesDialog() override; + + Glib::ustring getName() const + { + return "LayerPropertiesDialog"; + } + + static void showDialog(SPDesktop *desktop, double _amount, + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt, + bool _use_distance, + bool _aprox_radius, + Satellite _satellite); + +protected: + + SPDesktop *_desktop; + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity * + _knotpoint; + + Gtk::Label _fillet_chamfer_position_label; + Gtk::SpinButton _fillet_chamfer_position_numeric; + Gtk::RadioButton::Group _fillet_chamfer_type_group; + Gtk::RadioButton _fillet_chamfer_type_fillet; + Gtk::RadioButton _fillet_chamfer_type_inverse_fillet; + Gtk::RadioButton _fillet_chamfer_type_chamfer; + Gtk::RadioButton _fillet_chamfer_type_inverse_chamfer; + Gtk::Label _fillet_chamfer_chamfer_subdivisions_label; + Gtk::SpinButton _fillet_chamfer_chamfer_subdivisions; + + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static FilletChamferPropertiesDialog &_instance() + { + static FilletChamferPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt); + void _setUseDistance(bool use_knot_distance); + void _setAprox(bool aprox_radius); + void _setAmount(double amount); + void _setSatellite(Satellite satellite); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton *event); + + void _apply(); + void _close(); + bool _flexible; + Satellite _satellite; + bool _use_distance; + double _amount; + bool _aprox; + + friend class Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity; + +private: + FilletChamferPropertiesDialog( + FilletChamferPropertiesDialog const &); // no copy + FilletChamferPropertiesDialog &operator=( + FilletChamferPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + 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 : diff --git a/src/ui/dialog/lpe-powerstroke-properties.cpp b/src/ui/dialog/lpe-powerstroke-properties.cpp new file mode 100644 index 0000000..1e5d1b1 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington + * Andrius R. + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-powerstroke-properties.h" +#include +#include +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +PowerstrokePropertiesDialog::PowerstrokePropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _powerstroke_position_entry.set_activates_default(true); + _powerstroke_position_entry.set_digits(4); + _powerstroke_position_entry.set_increments(1,1); + _powerstroke_position_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_position_entry.set_hexpand(); + _powerstroke_position_label.set_label(_("Position:")); + _powerstroke_position_label.set_halign(Gtk::ALIGN_END); + _powerstroke_position_label.set_valign(Gtk::ALIGN_CENTER); + + _powerstroke_width_entry.set_activates_default(true); + _powerstroke_width_entry.set_digits(4); + _powerstroke_width_entry.set_increments(1,1); + _powerstroke_width_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_width_entry.set_hexpand(); + _powerstroke_width_label.set_label(_("Width:")); + _powerstroke_width_label.set_halign(Gtk::ALIGN_END); + _powerstroke_width_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_powerstroke_position_label,0,0,1,1); + _layout_table.attach(_powerstroke_position_entry,1,0,1,1); + _layout_table.attach(_powerstroke_width_label, 0,1,1,1); + _layout_table.attach(_powerstroke_width_entry, 1,1,1,1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)), + true + ) + ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_powerstroke_width_entry); +} + +PowerstrokePropertiesDialog::~PowerstrokePropertiesDialog() { + + _setDesktop(nullptr); +} + +void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog(); + + dialog->_setDesktop(desktop); + dialog->_setKnotPoint(knotpoint); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Node Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +PowerstrokePropertiesDialog::_apply() +{ + double d_pos = _powerstroke_position_entry.get_value(); + double d_width = _powerstroke_width_entry.get_value(); + _knotpoint->knot_set_offset(Geom::Point(d_pos, d_width)); + _close(); +} + +void +PowerstrokePropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun(&::operator delete), this), + false + ) + ); +} + +bool PowerstrokePropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void PowerstrokePropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void PowerstrokePropertiesDialog::_setKnotPoint(Geom::Point knotpoint) +{ + _powerstroke_position_entry.set_value(knotpoint.x()); + _powerstroke_width_entry.set_value(knotpoint.y()); +} + +void PowerstrokePropertiesDialog::_setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + _knotpoint = const_cast(pt); +} + +void PowerstrokePropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + + +/* + 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 : diff --git a/src/ui/dialog/lpe-powerstroke-properties.h b/src/ui/dialog/lpe-powerstroke-properties.h new file mode 100644 index 0000000..0bf6539 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H +#define INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H + +#include <2geom/point.h> +#include +#include "live_effects/parameter/powerstrokepointarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class PowerstrokePropertiesDialog : public Gtk::Dialog { + public: + PowerstrokePropertiesDialog(); + ~PowerstrokePropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + +protected: + + SPDesktop *_desktop; + Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *_knotpoint; + + Gtk::Label _powerstroke_position_label; + Gtk::SpinButton _powerstroke_position_entry; + Gtk::Label _powerstroke_width_label; + Gtk::SpinButton _powerstroke_width_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static PowerstrokePropertiesDialog &_instance() { + static PowerstrokePropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + + friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; + +private: + PowerstrokePropertiesDialog(PowerstrokePropertiesDialog const &); // no copy + PowerstrokePropertiesDialog &operator=(PowerstrokePropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + 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 : diff --git a/src/ui/dialog/memory.cpp b/src/ui/dialog/memory.cpp new file mode 100644 index 0000000..a4210ca --- /dev/null +++ b/src/ui/dialog/memory.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Memory statistics dialog. + */ +/* Authors: + * MenTaLguY + * + * Copyright (C) 2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/memory.h" +#include +#include + +#include +#include +#include + +#include "inkgc/gc-core.h" +#include "debug/heap.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +Glib::ustring format_size(std::size_t value) { + if (!value) { + return Glib::ustring("0"); + } + + typedef std::vector Digits; + typedef std::vector Groups; + + Groups groups; + + Digits *digits; + + while (value) { + unsigned places=3; + digits = new Digits(); + digits->reserve(places); + + while ( value && places ) { + digits->push_back('0' + (char)( value % 10 )); + value /= 10; + --places; + } + + groups.push_back(digits); + } + + Glib::ustring temp; + + while (true) { + digits = groups.back(); + while (!digits->empty()) { + temp.append(1, digits->back()); + digits->pop_back(); + } + delete digits; + + groups.pop_back(); + if (groups.empty()) { + break; + } + + temp.append(","); + } + + return temp; +} + +} + +struct Memory::Private { + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn used; + Gtk::TreeModelColumn slack; + Gtk::TreeModelColumn total; + + ModelColumns() { add(name); add(used); add(slack); add(total); } + }; + + Private() { + model = Gtk::ListStore::create(columns); + view.set_model(model); + view.append_column(_("Heap"), columns.name); + view.append_column(_("In Use"), columns.used); + // TRANSLATORS: "Slack" refers to memory which is in the heap but currently unused. + // More typical usage is to call this memory "free" rather than "slack". + view.append_column(_("Slack"), columns.slack); + view.append_column(_("Total"), columns.total); + } + + void update(); + + void start_update_task(); + void stop_update_task(); + + ModelColumns columns; + Glib::RefPtr model; + Gtk::TreeView view; + + sigc::connection update_task; +}; + +void Memory::Private::update() { + Debug::Heap::Stats total = { 0, 0 }; + + int aggregate_features = Debug::Heap::SIZE_AVAILABLE | Debug::Heap::USED_AVAILABLE; + Gtk::ListStore::iterator row; + + row = model->children().begin(); + + for ( unsigned i = 0 ; i < Debug::heap_count() ; i++ ) { + Debug::Heap *heap=Debug::get_heap(i); + if (heap) { + Debug::Heap::Stats stats=heap->stats(); + int features=heap->features(); + + aggregate_features &= features; + + if ( row == model->children().end() ) { + row = model->append(); + } + + row->set_value(columns.name, Glib::ustring(heap->name())); + if ( features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(stats.size)); + total.size += stats.size; + } else { + row->set_value(columns.total, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(stats.bytes_used)); + total.bytes_used += stats.bytes_used; + } else { + row->set_value(columns.used, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::SIZE_AVAILABLE && + features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(stats.size - stats.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + } + } + + if ( row == model->children().end() ) { + row = model->append(); + } + + Glib::ustring value; + + row->set_value(columns.name, Glib::ustring(_("Combined"))); + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(total.size)); + } else { + row->set_value(columns.total, Glib::ustring("> ") + format_size(total.size)); + } + + if ( aggregate_features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(total.bytes_used)); + } else { + row->set_value(columns.used, Glib::ustring("> ") + format_size(total.bytes_used)); + } + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE && + aggregate_features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(total.size - total.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + + while ( row != model->children().end() ) { + row = model->erase(row); + } +} + +void Memory::Private::start_update_task() { + update_task.disconnect(); + update_task = Glib::signal_timeout().connect( + sigc::bind_return(sigc::mem_fun(*this, &Private::update), true), + 500 + ); +} + +void Memory::Private::stop_update_task() { + update_task.disconnect(); +} + +Memory::Memory() + : UI::Widget::Panel("/dialogs/memory", SP_VERB_HELP_MEMORY), + _private(*(new Memory::Private())) +{ + _getContents()->pack_start(_private.view); + + _private.update(); + + addResponseButton(_("Recalculate"), Gtk::RESPONSE_APPLY); + + show_all_children(); + + signal_show().connect(sigc::mem_fun(_private, &Private::start_update_task)); + signal_hide().connect(sigc::mem_fun(_private, &Private::stop_update_task)); + + _private.start_update_task(); +} + +Memory::~Memory() { + delete &_private; +} + +void Memory::_apply() { + GC::Core::gcollect(); + _private.update(); +} + +} // 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 : diff --git a/src/ui/dialog/memory.h b/src/ui/dialog/memory.h new file mode 100644 index 0000000..5e6a917 --- /dev/null +++ b/src/ui/dialog/memory.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Memory statistics dialog + */ +/* Authors: + * MenTaLguY + * + * Copyright 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_DIALOG_MEMORY_H +#define SEEN_INKSCAPE_UI_DIALOG_MEMORY_H + +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Memory : public UI::Widget::Panel { +public: + Memory(); + ~Memory() override; + + static Memory &getInstance() { return *new Memory(); } + +protected: + void _apply() override; + +private: + Memory(Memory const &d) = delete; // no copy + void operator=(Memory const &d) = delete; // no assign + + struct Private; + Private &_private; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/ui/dialog/messages.cpp b/src/ui/dialog/messages.cpp new file mode 100644 index 0000000..85ba840 --- /dev/null +++ b/src/ui/dialog/messages.cpp @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Messages dialog - implementation. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "messages.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * Also a public method. Remove all text from the dialog + */ +void Messages::clear() +{ + Glib::RefPtr buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +Messages::Messages() + : UI::Widget::Panel("/dialogs/messages", SP_VERB_DIALOG_DEBUG), + buttonClear(_("_Clear"), _("Clear log messages")), + checkCapture(_("Capture log messages"), _("Capture log messages")) +{ + Gtk::Box *contents = _getContents(); + + /* + * Menu replaced with buttons + * + menuBar.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_File"), fileMenu) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_Clear"), + sigc::mem_fun(*this, &Messages::clear) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Capture log messages"), + sigc::mem_fun(*this, &Messages::captureLogMessages) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Release log messages"), + sigc::mem_fun(*this, &Messages::releaseLogMessages) ) ); + contents->pack_start(menuBar, Gtk::PACK_SHRINK); + */ + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + contents->pack_start(textScroll); + + buttonBox.set_spacing(6); + buttonBox.pack_start(checkCapture, true, true, 6); + buttonBox.pack_end(buttonClear, false, false, 10); + contents->pack_start(buttonBox, Gtk::PACK_SHRINK); + + // sick of this thing shrinking too much + set_size_request(400, 300); + + show_all_children(); + + message(_("Ready.")); + + buttonClear.signal_clicked().connect(sigc::mem_fun(*this, &Messages::clear)); + checkCapture.signal_clicked().connect(sigc::mem_fun(*this, &Messages::toggleCapture)); + + /* + * TODO - Setting this preference doesn't capture messages that the user can see. + * Inkscape creates an instance of a dialog on startup and sends messages there, but when the user + * opens the dialog View > Messages the DialogManager creates a new instance of this class that is not capturing messages. + * + * message(_("Enable log display by setting dialogs.debug 'redirect' attribute to 1 in preferences.xml")); + */ + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; + +} + +Messages::~Messages() += default; + + +//######################################################################### +//## M E T H O D S +//######################################################################### + +void Messages::message(char *msg) +{ + Glib::RefPtr buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +// dialogLoggingCallback is already used in debug.cpp +static void dialogLoggingCallback(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + Messages *dlg = static_cast(user_data); + dlg->message(const_cast(messageText)); + +} + +void Messages::toggleCapture() +{ + if (checkCapture.get_active()) { + captureLogMessages(); + } else { + releaseLogMessages(); + } +} + +void Messages::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + message(_("Log capture started.")); +} + +void Messages::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message(_("Log capture stopped.")); +} + +} //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 : diff --git a/src/ui/dialog/messages.h b/src/ui/dialog/messages.h new file mode 100644 index 0000000..b3ad393 --- /dev/null +++ b/src/ui/dialog/messages.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Messages dialog + * + * A very simple dialog for displaying Inkscape messages. Messages + * sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MESSAGES_H +#define INKSCAPE_UI_DIALOG_MESSAGES_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Messages : public UI::Widget::Panel { +public: + Messages(); + ~Messages() override; + + static Messages &getInstance() { return *new Messages(); } + + /** + * Clear all information from the dialog + */ + void clear(); + + /** + * Display a message + */ + void message(char *msg); + + /** + * Redirect g_log() messages to this widget + */ + void captureLogMessages(); + + /** + * Return g_log() messages to normal handling + */ + void releaseLogMessages(); + + void toggleCapture(); + +protected: + //Gtk::MenuBar menuBar; + //Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + Gtk::HBox buttonBox; + Gtk::Button buttonClear; + Gtk::CheckButton checkCapture; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; + +private: + Messages(Messages const &d) = delete; + Messages operator=(Messages const &d) = delete; +}; + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_MESSAGES_H + +/* + 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 : diff --git a/src/ui/dialog/new-from-template.cpp b/src/ui/dialog/new-from-template.cpp new file mode 100644 index 0000000..bfccdb6 --- /dev/null +++ b/src/ui/dialog/new-from-template.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog - implementation + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "new-from-template.h" +#include "file.h" + +#include "include/gtkmm_version.h" + +namespace Inkscape { +namespace UI { + + +NewFromTemplate::NewFromTemplate() + : _create_template_button(_("Create from template")) +{ + set_title(_("New From Template")); + resize(400, 400); + + _main_widget = new TemplateLoadTab(this); + + get_content_area()->pack_start(*_main_widget); + + _create_template_button.set_halign(Gtk::ALIGN_END); + _create_template_button.set_valign(Gtk::ALIGN_END); + _create_template_button.set_margin_end(15); + + get_content_area()->pack_end(_create_template_button, Gtk::PACK_SHRINK); + + _create_template_button.signal_clicked().connect( + sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate)); + _create_template_button.set_sensitive(false); + + show_all(); +} + +NewFromTemplate::~NewFromTemplate() +{ + delete _main_widget; +} + +void NewFromTemplate::setCreateButtonSensitive(bool value) +{ + _create_template_button.set_sensitive(value); +} + +void NewFromTemplate::_createFromTemplate() +{ + _main_widget->createTemplate(); + _onClose(); +} + +void NewFromTemplate::_onClose() +{ + response(0); +} + +void NewFromTemplate::load_new_from_template() +{ + NewFromTemplate dl; + dl.run(); +} + +} +} diff --git a/src/ui/dialog/new-from-template.h b/src/ui/dialog/new-from-template.h new file mode 100644 index 0000000..a308fe4 --- /dev/null +++ b/src/ui/dialog/new-from-template.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H + +#include +#include + +#include "template-load-tab.h" + + +namespace Inkscape { +namespace UI { + + +class NewFromTemplate : public Gtk::Dialog +{ + +friend class TemplateLoadTab; +public: + static void load_new_from_template(); + void setCreateButtonSensitive(bool value); + ~NewFromTemplate() override; + +private: + NewFromTemplate(); + Gtk::Button _create_template_button; + TemplateLoadTab* _main_widget; + + void _createFromTemplate(); + void _onClose(); +}; + +} +} +#endif diff --git a/src/ui/dialog/object-attributes.cpp b/src/ui/dialog/object-attributes.cpp new file mode 100644 index 0000000..f129854 --- /dev/null +++ b/src/ui/dialog/object-attributes.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include + +#include "desktop.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-anchor.h" +#include "object/sp-image.h" + +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/dialog-manager.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct SPAttrDesc { + gchar const *label; + gchar const *attribute; +}; + +static const SPAttrDesc anchor_desc[] = { + { N_("Href:"), "xlink:href"}, + { N_("Target:"), "target"}, + { N_("Type:"), "xlink:type"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute + // Identifies the type of the related resource with an absolute URI + { N_("Role:"), "xlink:role"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute + // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link. + { N_("Arcrole:"), "xlink:arcrole"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute + { N_("Title:"), "xlink:title"}, + { N_("Show:"), "xlink:show"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute + { N_("Actuate:"), "xlink:actuate"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_desc[] = { + { N_("URL:"), "xlink:href"}, + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_nohref_desc[] = { + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +ObjectAttributes::ObjectAttributes () : + UI::Widget::Panel("/dialogs/objectattr/", SP_VERB_DIALOG_ATTR), + blocked (false), + CurrentItem(nullptr), + attrTable(Gtk::manage(new SPAttributeTable())), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + attrTable->show(); + widget_setup(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectAttributes::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +ObjectAttributes::~ObjectAttributes () +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void ObjectAttributes::widget_setup () +{ + if (blocked) + { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + SPItem *item = selection->singleItem(); + if (!item) + { + set_sensitive (false); + CurrentItem = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + return; + } + + blocked = true; + + // CPPIFY + SPObject *obj = item; //to get the selected item +// GObjectClass *klass = G_OBJECT_GET_CLASS(obj); //to deduce the object's type +// GType type = G_TYPE_FROM_CLASS(klass); + const SPAttrDesc *desc; + +// if (type == SP_TYPE_ANCHOR) + if (SP_IS_ANCHOR(item)) + { + desc = anchor_desc; + } +// else if (type == SP_TYPE_IMAGE) + else if (SP_IS_IMAGE(item)) + { + Inkscape::XML::Node *ir = obj->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) + { + desc = image_nohref_desc; + } + else + { + desc = image_desc; + } + } + else + { + blocked = false; + set_sensitive (false); + return; + } + + std::vector labels; + std::vector attrs; + if (CurrentItem != item) + { + int len = 0; + while (desc[len].label) + { + labels.emplace_back(desc[len].label); + attrs.emplace_back(desc[len].attribute); + len += 1; + } + attrTable->set_object(obj, labels, attrs, (GtkWidget*)gobj()); + CurrentItem = item; + } + else + { + attrTable->change_object(obj); + } + + set_sensitive (true); + show_all(); + blocked = false; +} + +void ObjectAttributes::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + + // Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &ObjectAttributes::selectionModifiedCB))); + } + widget_setup(); + } +} + +void ObjectAttributes::selectionModifiedCB( guint flags ) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + attrTable->reread_properties(); + } +} + + +} +} +} + +/* + 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 : diff --git a/src/ui/dialog/object-attributes.h b/src/ui/dialog/object-attributes.h new file mode 100644 index 0000000..ad718ea --- /dev/null +++ b/src/ui/dialog/object-attributes.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_OBJECT_ATTRIBUTES_H +#define SEEN_DIALOGS_OBJECT_ATTRIBUTES_H + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +class SPAttributeTable; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object attributes (currently for images and links). + */ +class ObjectAttributes : public Widget::Panel { +public: + ObjectAttributes (); + ~ObjectAttributes () override; + + /** + * Returns a new instance of the object attributes dialog. + * + * Auxiliary function needed by the DialogManager. + */ + static ObjectAttributes &getInstance() { return *new ObjectAttributes(); } + + /** + * Updates entries and other child widgets on selection change, object modification, etc. + */ + void widget_setup(); + +private: + /** + * Is UI update bloched? + */ + bool blocked; + + /** + * Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected). + */ + SPItem *CurrentItem; + + /** + * Child widget to show the object attributes. + * + * attrTable makes the labels and edit boxes for the attributes defined + * in the SPAttrDesc arrays at the top of the cpp-file. This widgets also + * ensures object attribute modifications by the user are set. + */ + SPAttributeTable *attrTable; + + /** + * Stores the current desktop. + */ + SPDesktop *desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + + /** + * Link to callback function for a selection change. + */ + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + + /** + * Link to callback function for a modification of the selected object. + */ + sigc::connection selectModifiedConn; + + /** + * Callback function invoked by the desktop tracker in case of a modification of the selected object. + */ + void selectionModifiedCB( guint flags ); + + /* + * Can be invoked for setting the desktop. Currently not used. + */ + // void setDesktop(SPDesktop *desktop); + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + +}; + +} +} +} + +#endif + +/* + 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 : diff --git a/src/ui/dialog/object-properties.cpp b/src/ui/dialog/object-properties.cpp new file mode 100644 index 0000000..b2594a0 --- /dev/null +++ b/src/ui/dialog/object-properties.cpp @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem + * c++ version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Abhishek Sharma + */ + +#include "object-properties.h" + +#include + +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" +#include "style.h" +#include "style-enums.h" + +#include "object/sp-image.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ObjectProperties::ObjectProperties() + : UI::Widget::Panel("/dialogs/object/", SP_VERB_DIALOG_ITEM) + , _blocked(false) + , _current_item(nullptr) + , _label_id(_("_ID:"), true) + , _label_label(_("_Label:"), true) + , _label_title(_("_Title:"), true) + , _label_dpi(_("_DPI SVG:"), true) + , _label_image_rendering(_("_Image Rendering:"), true) + , _cb_hide(_("_Hide"), true) + , _cb_lock(_("L_ock"), true) + , _cb_aspect_ratio(_("Preserve Ratio"), true) + , _exp_interactivity(_("_Interactivity"), true) + , _attr_table(Gtk::manage(new SPAttributeTable())) + , _desktop(nullptr) +{ + //initialize labels for the table at the bottom of the dialog + _int_attrs.emplace_back("onclick"); + _int_attrs.emplace_back("onmouseover"); + _int_attrs.emplace_back("onmouseout"); + _int_attrs.emplace_back("onmousedown"); + _int_attrs.emplace_back("onmouseup"); + _int_attrs.emplace_back("onmousemove"); + _int_attrs.emplace_back("onfocusin"); + _int_attrs.emplace_back("onfocusout"); + _int_attrs.emplace_back("onload"); + + _int_labels.emplace_back("onclick:"); + _int_labels.emplace_back("onmouseover:"); + _int_labels.emplace_back("onmouseout:"); + _int_labels.emplace_back("onmousedown:"); + _int_labels.emplace_back("onmouseup:"); + _int_labels.emplace_back("onmousemove:"); + _int_labels.emplace_back("onfocusin:"); + _int_labels.emplace_back("onfocusout:"); + _int_labels.emplace_back("onload:"); + + _desktop_changed_connection = _desktop_tracker.connectDesktopChanged( + sigc::mem_fun(*this, &ObjectProperties::_setTargetDesktop) + ); + _desktop_tracker.connect(GTK_WIDGET(gobj())); + + _init(); +} + +ObjectProperties::~ObjectProperties() +{ + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + _desktop_changed_connection.disconnect(); + _desktop_tracker.disconnect(); +} + +void ObjectProperties::_init() +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + + auto grid_top = Gtk::manage(new Gtk::Grid()); + grid_top->set_row_spacing(4); + grid_top->set_column_spacing(0); + grid_top->set_border_width(4); + + contents->pack_start(*grid_top, false, false, 0); + + + /* Create the label for the object id */ + _label_id.set_label(_label_id.get_label() + " "); + _label_id.set_halign(Gtk::ALIGN_START); + _label_id.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_id, 0, 0, 1, 1); + + /* Create the entry box for the object id */ + _entry_id.set_tooltip_text(_("The id= attribute (only letters, digits, and the characters .-_: allowed)")); + _entry_id.set_max_length(64); + _entry_id.set_hexpand(); + _entry_id.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_id, 1, 0, 1, 1); + + _label_id.set_mnemonic_widget(_entry_id); + + // pressing enter in the id field is the same as clicking Set: + _entry_id.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + // focus is in the id field initially: + _entry_id.grab_focus(); + + + /* Create the label for the object label */ + _label_label.set_label(_label_label.get_label() + " "); + _label_label.set_halign(Gtk::ALIGN_START); + _label_label.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_label, 0, 1, 1, 1); + + /* Create the entry box for the object label */ + _entry_label.set_tooltip_text(_("A freeform label for the object")); + _entry_label.set_max_length(256); + + _entry_label.set_hexpand(); + _entry_label.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_label, 1, 1, 1, 1); + + _label_label.set_mnemonic_widget(_entry_label); + + // pressing enter in the label field is the same as clicking Set: + _entry_label.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + + /* Create the label for the object title */ + _label_title.set_label(_label_title.get_label() + " "); + _label_title.set_halign(Gtk::ALIGN_START); + _label_title.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_title, 0, 2, 1, 1); + + /* Create the entry box for the object title */ + _entry_title.set_sensitive (FALSE); + _entry_title.set_max_length (256); + + _entry_title.set_hexpand(); + _entry_title.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_title, 1, 2, 1, 1); + + _label_title.set_mnemonic_widget(_entry_title); + // pressing enter in the label field is the same as clicking Set: + _entry_title.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Create the frame for the object description */ + Gtk::Label *label_desc = Gtk::manage(new Gtk::Label(_("_Description:"), true)); + UI::Widget::Frame *frame_desc = Gtk::manage(new UI::Widget::Frame("", FALSE)); + frame_desc->set_label_widget(*label_desc); + frame_desc->set_padding (0,0,0,0); + contents->pack_start(*frame_desc, true, true, 0); + + /* Create the text view box for the object description */ + _ft_description.set_border_width(4); + _ft_description.set_sensitive(FALSE); + frame_desc->add(_ft_description); + _ft_description.set_shadow_type(Gtk::SHADOW_IN); + + _tv_description.set_wrap_mode(Gtk::WRAP_WORD); + _tv_description.get_buffer()->set_text(""); + _ft_description.add(_tv_description); + _tv_description.add_mnemonic_label(*label_desc); + + /* Create the label for the object title */ + _label_dpi.set_label(_label_dpi.get_label() + " "); + _label_dpi.set_halign(Gtk::ALIGN_START); + _label_dpi.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_dpi, 0, 3, 1, 1); + + /* Create the entry box for the SVG DPI */ + _spin_dpi.set_digits(2); + _spin_dpi.set_range(1, 1200); + grid_top->attach(_spin_dpi, 1, 3, 1, 1); + + _label_dpi.set_mnemonic_widget(_spin_dpi); + // pressing enter in the label field is the same as clicking Set: + _spin_dpi.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Image rendering */ + /* Create the label for the object ImageRendering */ + _label_image_rendering.set_label(_label_image_rendering.get_label() + " "); + _label_image_rendering.set_halign(Gtk::ALIGN_START); + _label_image_rendering.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_image_rendering, 0, 4, 1, 1); + + /* Create the combo box text for the 'image-rendering' property */ + for (unsigned i = 0; enum_image_rendering[i].key; ++i) { + _combo_image_rendering.append(enum_image_rendering[i].key); + } + _combo_image_rendering.set_tooltip_text(_("The 'image-rendering' property can influence how a bitmap is re-scaled:\n" + "\t• 'auto' no preference (usually smooth but blurred)\n" + "\t• 'optimizeQuality' prefer rendering quality (usually smooth but blurred)\n" + "\t• 'optimizeSpeed' prefer rendering speed (usually blocky)\n" + "\t• 'crisp-edges' rescale without blurring edges (often blocky)\n" + "\t• 'pixelated' render blocky\n" + "Note that the specification of this property is not finalized. " + "Support and interpretation of these values varies between renderers.")); + + _combo_image_rendering.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_combo_image_rendering, 1, 4, 1, 1); + + _label_image_rendering.set_mnemonic_widget(_combo_image_rendering); + + _combo_image_rendering.signal_changed().connect( + sigc::mem_fun(this, &ObjectProperties::_imageRenderingChanged) + ); + + + + /* Check boxes */ + Gtk::HBox *hb_checkboxes = Gtk::manage(new Gtk::HBox()); + contents->pack_start(*hb_checkboxes, Gtk::PACK_SHRINK, 0); + + auto grid_cb = Gtk::manage(new Gtk::Grid()); + grid_cb->set_row_homogeneous(); + grid_cb->set_column_homogeneous(true); + + grid_cb->set_border_width(4); + hb_checkboxes->pack_start(*grid_cb, true, true, 0); + + /* Hide */ + _cb_hide.set_tooltip_text (_("Check to make the object invisible")); + _cb_hide.set_hexpand(); + _cb_hide.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_hide, 0, 0, 1, 1); + + _cb_hide.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_hiddenToggled)); + + /* Lock */ + // TRANSLATORS: "Lock" is a verb here + _cb_lock.set_tooltip_text(_("Check to make the object insensitive (not selectable by mouse)")); + _cb_lock.set_hexpand(); + _cb_lock.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_lock, 1, 0, 1, 1); + + _cb_lock.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_sensitivityToggled)); + + /* Preserve aspect ratio */ + _cb_aspect_ratio.set_tooltip_text(_("Check to preserve aspect ratio on images")); + _cb_aspect_ratio.set_hexpand(); + _cb_aspect_ratio.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_aspect_ratio, 0, 1, 1, 1); + + _cb_aspect_ratio.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_aspectRatioToggled)); + + + /* Button for setting the object's id, label, title and description. */ + Gtk::Button *btn_set = Gtk::manage(new Gtk::Button(_("_Set"), true)); + btn_set->set_hexpand(); + btn_set->set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(*btn_set, 1, 1, 1, 1); + + btn_set->signal_clicked().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Interactivity options */ + _exp_interactivity.set_vexpand(false); + contents->pack_start(_exp_interactivity, Gtk::PACK_SHRINK); + + show_all(); + update(); +} + +void ObjectProperties::update() +{ + if (_blocked || !_desktop) { + return; + } + if (SP_ACTIVE_DESKTOP != _desktop) { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + Gtk::Box *contents = _getContents(); + + if (!selection->singleItem()) { + contents->set_sensitive (false); + _current_item = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + _attr_table->clear(); + return; + } else { + contents->set_sensitive (true); + } + + SPItem *item = selection->singleItem(); + if (_current_item == item) + { + //otherwise we would end up wasting resources through the modify selection + //callback when moving an object (endlessly setting the labels and recreating _attr_table) + return; + } + _blocked = true; + _cb_aspect_ratio.set_active(g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0); + _cb_lock.set_active(item->isLocked()); /* Sensitive */ + _cb_hide.set_active(item->isExplicitlyHidden()); /* Hidden */ + + if (item->cloned) { + /* ID */ + _entry_id.set_text(""); + _entry_id.set_sensitive(FALSE); + _label_id.set_text(_("Ref")); + + /* Label */ + _entry_label.set_text(""); + _entry_label.set_sensitive(FALSE); + _label_label.set_text(_("Ref")); + + } else { + SPObject *obj = static_cast(item); + + /* ID */ + _entry_id.set_text(obj->getId() ? obj->getId() : ""); + _entry_id.set_sensitive(TRUE); + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + + /* Label */ + char const *currentlabel = obj->label(); + char const *placeholder = ""; + if (!currentlabel) { + currentlabel = ""; + placeholder = obj->defaultLabel(); + } + _entry_label.set_text(currentlabel); + _entry_label.set_placeholder_text(placeholder); + _entry_label.set_sensitive(TRUE); + + /* Title */ + gchar *title = obj->title(); + if (title) { + _entry_title.set_text(title); + g_free(title); + } + else { + _entry_title.set_text(""); + } + _entry_title.set_sensitive(TRUE); + + /* Image Rendering */ + if (SP_IS_IMAGE(item)) { + _combo_image_rendering.show(); + _label_image_rendering.show(); + _combo_image_rendering.set_active(obj->style->image_rendering.value); + if (obj->getAttribute("inkscape:svg-dpi")) { + _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi"))); + _spin_dpi.show(); + _label_dpi.show(); + } else { + _spin_dpi.hide(); + _label_dpi.hide(); + } + } else { + _combo_image_rendering.hide(); + _combo_image_rendering.unset_active(); + _label_image_rendering.hide(); + _spin_dpi.hide(); + _label_dpi.hide(); + } + + /* Description */ + gchar *desc = obj->desc(); + if (desc) { + _tv_description.get_buffer()->set_text(desc); + g_free(desc); + } else { + _tv_description.get_buffer()->set_text(""); + } + _ft_description.set_sensitive(TRUE); + + if (_current_item == nullptr) { + _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj()); + } else { + _attr_table->change_object(obj); + } + _attr_table->show_all(); + } + _current_item = item; + _blocked = false; +} + +void ObjectProperties::_labelChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + /* Retrieve the label widget for the object's id */ + gchar *id = g_strdup(_entry_id.get_text().c_str()); + g_strcanon(id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_'); + if (g_strcmp0(id, item->getId()) == 0) { + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + } else if (!*id || !isalnum (*id)) { + _label_id.set_text(_("Id invalid! ")); + } else if (SP_ACTIVE_DOCUMENT->getObjectById(id) != nullptr) { + _label_id.set_text(_("Id exists! ")); + } else { + SPException ex; + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + SP_EXCEPTION_INIT(&ex); + item->setAttribute("id", id, &ex); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set object ID")); + } + g_free(id); + + /* Retrieve the label widget for the object's label */ + Glib::ustring label = _entry_label.get_text(); + + /* Give feedback on success of setting the drawing object's label + * using the widget's label text + */ + SPObject *obj = static_cast(item); + char const *currentlabel = obj->label(); + if (label.compare(currentlabel ? currentlabel : "")) { + obj->setLabel(label.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object label")); + } + + /* Retrieve the title */ + if (obj->setTitle(_entry_title.get_text().c_str())) { + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object title")); + } + + /* Retrieve the DPI */ + if (SP_IS_IMAGE(obj)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + obj->setAttribute("inkscape:svg-dpi", dpi_value); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set image DPI")); + } + + /* Retrieve the description */ + Gtk::TextBuffer::iterator start, end; + _tv_description.get_buffer()->get_bounds(start, end); + Glib::ustring desc = _tv_description.get_buffer()->get_text(start, end, TRUE); + if (obj->setDesc(desc.c_str())) { + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object description")); + } + + _blocked = false; +} + +void ObjectProperties::_imageRenderingChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + Glib::ustring scale = _combo_image_rendering.get_active_text(); + + // We should unset if the parent computed value is auto and the desired value is auto. + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + Inkscape::XML::Node *image_node = item->getRepr(); + if (image_node) { + sp_repr_css_change(image_node, css, "style"); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set image rendering option")); + } + sp_repr_css_attr_unref(css); + + _blocked = false; +} + +void ObjectProperties::_sensitivityToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setLocked(_cb_lock.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_lock.get_active() ? _("Lock object") : _("Unlock object")); + _blocked = false; +} + +void ObjectProperties::_aspectRatioToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + + const char *active; + if (_cb_aspect_ratio.get_active()) { + active = "xMidYMid"; + } + else { + active = "none"; + } + /* Retrieve the DPI */ + if (SP_IS_IMAGE(item)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + item->setAttribute("preserveAspectRatio", active); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set preserve ratio")); + } + _blocked = false; +} + +void ObjectProperties::_hiddenToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setExplicitlyHidden(_cb_hide.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_hide.get_active() ? _("Hide object") : _("Unhide object")); + _blocked = false; +} + +void ObjectProperties::_setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + _desktop_tracker.setBase(desktop); +} + +void ObjectProperties::_setTargetDesktop(SPDesktop *desktop) +{ + if (this->_desktop != desktop) { + if (this->_desktop) { + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + } + this->_desktop = desktop; + if (desktop && desktop->selection) { + _selection_changed_connection = desktop->selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + _subselection_changed_connection = desktop->connectToolSubselectionChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + } + update(); + } +} + +} +} +} + +/* + 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 : diff --git a/src/ui/dialog/object-properties.h b/src/ui/dialog/object-properties.h new file mode 100644 index 0000000..a1974c0 --- /dev/null +++ b/src/ui/dialog/object-properties.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem + * c++version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Abhishek Sharma + */ + +#ifndef SEEN_DIALOGS_ITEM_PROPERTIES_H +#define SEEN_DIALOGS_ITEM_PROPERTIES_H + +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "ui/dialog/desktop-tracker.h" + +class SPAttributeTable; +class SPDesktop; +class SPItem; + +namespace Gtk { +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object properties. + * + * A widget to enter an ID, label, title and description for an object. + * In addition it allows to edit the properties of an object. + */ +class ObjectProperties : public Widget::Panel { +public: + ObjectProperties(); + ~ObjectProperties() override; + + static ObjectProperties &getInstance() { return *new ObjectProperties(); } + + /// Updates entries and other child widgets on selection change, object modification, etc. + void update(); + +private: + bool _blocked; + SPItem *_current_item; //to store the current item, for not wasting resources + std::vector _int_attrs; + std::vector _int_labels; + + Gtk::Label _label_id; //the label for the object ID + Gtk::Entry _entry_id; //the entry for the object ID + Gtk::Label _label_label; //the label for the object label + Gtk::Entry _entry_label; //the entry for the object label + Gtk::Label _label_title; //the label for the object title + Gtk::Entry _entry_title; //the entry for the object title + + Gtk::Label _label_image_rendering; // the label for 'image-rendering' + Gtk::ComboBoxText _combo_image_rendering; // the combo box text for 'image-rendering' + + Gtk::Frame _ft_description; //the frame for the text of the object description + Gtk::TextView _tv_description; //the text view object showing the object description + + Gtk::CheckButton _cb_hide; //the check button hide + Gtk::CheckButton _cb_lock; //the check button lock + Gtk::CheckButton _cb_aspect_ratio; //the preserve aspect ratio of images + + Gtk::Label _label_dpi; //the entry for the dpi value + Gtk::SpinButton _spin_dpi; //the expander for interactivity + Gtk::Expander _exp_interactivity; //the expander for interactivity + SPAttributeTable *_attr_table; //the widget for showing the on... names at the bottom + + SPDesktop *_desktop; + DesktopTracker _desktop_tracker; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + sigc::connection _subselection_changed_connection; + + /// Constructor auxiliary function creating the child widgets. + void _init(); + + /// Sets object properties (ID, label, title, description) on user input. + void _labelChanged(); + + /// Callback for 'image-rendering'. + void _imageRenderingChanged(); + + /// Callback for checkbox Lock. + void _sensitivityToggled(); + + /// Callback for checkbox Hide. + void _hiddenToggled(); + + /// Callback for checkbox Preserve Aspect Ratio. + void _aspectRatioToggled(); + + /// Can be invoked for setting the desktop. Currently not used. + void _setDesktop(SPDesktop *desktop); + + /// Is invoked by the desktop tracker when the desktop changes. + void _setTargetDesktop(SPDesktop *desktop); +}; + +} +} +} + +#endif + +/* + 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 : diff --git a/src/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp new file mode 100644 index 0000000..f9e14e7 --- /dev/null +++ b/src/ui/dialog/objects.cpp @@ -0,0 +1,2363 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative) + * + * Authors: + * Theodore Janeczko + * Tweaked by Liam P White for use in Inkscape + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 + * Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "objects.h" + +#include +#include +#include +#include + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "shortcuts.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/gaussian-blur.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "style.h" + +#include "ui/contextmenu.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/selected-color.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/clipmaskicon.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/highlight-picker.h" +#include "ui/widget/imagetoggler.h" +#include "ui/widget/insertordericon.h" +#include "ui/widget/layertypeicon.h" + +#include "xml/node-observer.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::XML::Node; + +/** + * Gets an instance of the Objects panel + */ +ObjectsPanel& ObjectsPanel::getInstance() +{ + return *new ObjectsPanel(); +} + +/** + * Column enumeration + */ +enum { + COL_VISIBLE = 1, + COL_LOCKED, + COL_TYPE, +// COL_INSERTORDER, + COL_CLIPMASK, + COL_HIGHLIGHT +}; + +/** + * Button enumeration + */ +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + BUTTON_SETCLIP, + BUTTON_CLIPGROUP, +// BUTTON_SETINVCLIP, + BUTTON_UNSETCLIP, + BUTTON_SETMASK, + BUTTON_UNSETMASK, + BUTTON_GROUP, + BUTTON_UNGROUP, + BUTTON_COLLAPSE_ALL, + DRAGNDROP, + UPDATE_TREE +}; + +/** + * Xml node observer for observing objects in the document + */ +class ObjectsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { +public: + /** + * Creates a new object watcher + * @param pnl The panel to which the object watcher belongs + * @param obj The object to watch + */ + ObjectWatcher(ObjectsPanel* pnl, SPObject* obj) : + _pnl(pnl), + _obj(obj), + _repr(obj->getRepr()), + _highlightAttr(g_quark_from_string("inkscape:highlight-color")), + _lockedAttr(g_quark_from_string("sodipodi:insensitive")), + _labelAttr(g_quark_from_string("inkscape:label")), + _groupAttr(g_quark_from_string("inkscape:groupmode")), + _styleAttr(g_quark_from_string("style")), + _clipAttr(g_quark_from_string("clip-path")), + _maskAttr(g_quark_from_string("mask")) + { + _repr->addObserver(*this); + } + + ~ObjectWatcher() override { + _repr->removeObserver(*this); + } + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + /* Weird things happen on undo! we get notified about the child being removed, but after that we still get + * notified for attributes being changed on this XML node! In that case the corresponding SPObject might already + * have been deleted and the pointer to might be invalid, leading to a segfault if we're not carefull. + * So after we initiated the update of the treeview using _objectsChangedWrapper() in notifyChildRemoved(), the + * _pending_update flag is set, and we will no longer process any notifyAttributeChanged() + * Reproducing the crash: new document -> open objects panel -> draw freehand line -> undo -> segfault (but only + * if we don't check for _pending_update) */ + if ( _pnl && (!_pnl->_pending_update) && _obj ) { + if ( name == _lockedAttr || name == _labelAttr || name == _highlightAttr || name == _groupAttr || name == _styleAttr || name == _clipAttr || name == _maskAttr ) { + _pnl->_updateObject(_obj, name == _highlightAttr); + if ( name == _styleAttr ) { + _pnl->_updateComposite(); + } + } + } + } + + /** + * Objects panel to which this watcher belongs + */ + ObjectsPanel* _pnl; + + /** + * The object that is being observed + */ + SPObject* _obj; + + /** + * The xml representation of the object that is being observed + */ + Inkscape::XML::Node* _repr; + + /* These are quarks which define the attributes that we are observing */ + GQuark _highlightAttr; + GQuark _lockedAttr; + GQuark _labelAttr; + GQuark _groupAttr; + GQuark _styleAttr; + GQuark _clipAttr; + GQuark _maskAttr; +}; + +class ObjectsPanel::InternalUIBounce +{ +public: + int _actionCode; + sigc::connection _signal; +}; + +class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + add(_colType); + add(_colHighlight); + add(_colClipMask); + add(_colPrevSelectionState); + //add(_colInsertOrder); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colVisible; + Gtk::TreeModelColumn _colLocked; + Gtk::TreeModelColumn _colType; + Gtk::TreeModelColumn _colHighlight; + Gtk::TreeModelColumn _colClipMask; + Gtk::TreeModelColumn _colPrevSelectionState; + //Gtk::TreeModelColumn _colInsertOrder; +}; + +/** + * Stylizes a button using the given icon name and tooltip + */ +void ObjectsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + +/** + * Adds an item to the pop-up (right-click) menu + * @param desktop The active destktop + * @param code Action code + * @param id Button id for callback function + * @return The generated menu item + */ +Gtk::MenuItem& ObjectsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +/** + * Attach a watcher to the XML node of an item, which will signal us in case of changes to that item or node + * @param item The item of which the XML node is to be watched + */ +void ObjectsPanel::_addWatcher(SPItem *item) { + bool used = true; // Any newly created watcher is obviously being used + auto iter = _objectWatchers.find(item); + if (iter == _objectWatchers.end()) { // If not found then watcher doesn't exist yet + ObjectsPanel::ObjectWatcher *w = new ObjectsPanel::ObjectWatcher(this, item); + _objectWatchers.emplace(item, std::make_pair(w, used)); + } else { // Found; no need to create a new watcher; just flag it as "in use" + (*iter).second.second = used; + } +} + +/** + * Delete the watchers, which signal us in case of changes to the item being watched + * @param only_unused Only delete those watchers that are no longer in use + */ +void ObjectsPanel::_removeWatchers(bool only_unused = false) { + // Delete all watchers (optionally only those which are not in use) + auto iter = _objectWatchers.begin(); + while (iter != _objectWatchers.end()) { + bool used = (*iter).second.second; + bool delete_watcher = (!only_unused) || (only_unused && !used); + if ( delete_watcher ) { + ObjectsPanel::ObjectWatcher *w = (*iter).second.first; + delete w; + iter = _objectWatchers.erase(iter); + } else { + // It must be in use, so the used "field" should be set to true; + // However, when _removeWatchers is being called, we will already have processed the complete queue ... + g_assert(_tree_update_queue.empty()); + // .. and we can preemptively flag it as unused for the processing of the next queue + (*iter).second.second = false; // It will be set to true again by _addWatcher, if in use + iter++; + } + } +} +/** + * Call function for asynchronous invocation of _objectsChanged + */ +void ObjectsPanel::_objectsChangedWrapper(SPObject */*obj*/) { + // We used to call _objectsChanged with a reference to _obj, + // but since _obj wasn't used, I'm dropping that for now + _takeAction(UPDATE_TREE); +} + +/** + * Callback function for when an object changes. Essentially refreshes the entire tree + * @param obj Object which was changed (currently not used as the entire tree is recreated) + */ +void ObjectsPanel::_objectsChanged(SPObject */*obj*/) +{ + if (_desktop) { + //Get the current document's root and use that to enumerate the tree + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); // Will be unblocked after the queue has been processed fully + _documentChangedCurrentLayer.block(); + + //Clear the tree store + _store->clear(); // This will increment it's stamp, making all old iterators + _tree_cache.clear(); // invalid. So we will also clear our own cache, as well + _tree_update_queue.clear(); // as any remaining update queue + + // Temporarily detach the TreeStore from the TreeView to slightly reduce flickering, and to speed up + // Note: if we truly want to eliminate the flickering, we should implement double buffering on the _store, + // but maybe this is a bit too much effort/bloat for too little gain? + _tree.unset_model(); + + //Add all items recursively; we will do this asynchronously, by first filling a queue, which is rather fast + _queueObject( root, nullptr ); + //However, the processing of this queue is slow, so this is done at a low priority and in small chunks. Using + //only small chunks keeps Inkscape responsive, for example while using the spray tool. After processing each + //of the chunks, Inkscape will check if there are other tasks with a high priority, for example when user is + //spraying. If so, the sprayed objects will be added first, and the whole updating will be restarted before + //it even finished. + _paths_to_be_expanded.clear(); + _processQueue_sig.disconnect(); // Might be needed in case objectsChanged is called directly, and not through objectsChangedWrapper() + _processQueue_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_processQueue), 0, Glib::PRIORITY_DEFAULT_IDLE+100); + } + } +} + +/** + * Recursively adds the children of the given item to the tree + * @param obj Root object to add to the tree + * @param parentRow Parent tree row (or NULL if adding to tree root) + */ +void ObjectsPanel::_queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow) +{ + bool already_expanded = false; + + for(auto& child: obj->children) { + if (SP_IS_ITEM(&child)) { + //Add the item to the tree, basically only creating an empty row in the tree view + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + + //Add the item to a queue, so we can fill in the data in each row asynchronously + //at a later stage. See the comments in _objectsChanged() for more details + bool expand = SP_IS_GROUP(obj) && SP_GROUP(obj)->expanded() && (not already_expanded); + _tree_update_queue.emplace_back(SP_ITEM(&child), iter, expand); + + already_expanded = expand || already_expanded; // We need to expand only a single child in each group + + //If the item is a group, recursively add its children + if (SP_IS_GROUP(&child)) { + Gtk::TreeModel::Row row = *iter; + _queueObject(&child, &row); + } + } + } +} + +/** + * Walks through the queue in small chunks, and fills in the rows in the tree view accordingly + * @return False if the queue has been fully emptied + */ +bool ObjectsPanel::_processQueue() { + auto queue_iter = _tree_update_queue.begin(); + auto queue_end = _tree_update_queue.end(); + int count = 0; + + while (queue_iter != queue_end) { + //The queue is a list of tuples; expand the tuples + SPItem *item = std::get<0>(*queue_iter); + Gtk::TreeModel::iterator iter = std::get<1>(*queue_iter); + bool expanded = std::get<2>(*queue_iter); + //Add the object to the tree view and tree cache + _addObjectToTree(item, *iter, expanded); + _tree_cache.emplace(item, *iter); + + /* Update the watchers; No watcher shall be deleted before the processing of the queue has + * finished; we need to keep watching for items that might have been deleted while the queue, + * which is being processed on idle, was not yet empty. This is because when an item is deleted, the + * queue is still holding a pointer to it. The NotifyChildRemoved method of the watcher will stop the + * processing of the queue and prevent a segmentation fault, but only if there is a watcher in place*/ + _addWatcher(item); + + queue_iter = _tree_update_queue.erase(queue_iter); + count++; + if (count == 100 && (!_tree_update_queue.empty())) { + return true; // we have not yet reached the end of the queue, so return true to keep the timeout signal alive + } + } + + //We have reached the end of the queue, and it is safe to remove any watchers + _removeWatchers(true); // ... but only remove those that are no longer in use + + // Now we can bring the tree view back to life safely + _tree.set_model(_store); // Attach the store again to the tree view this sets search columns as -1 + _tree.set_search_column(_model->_colLabel);//set search column again + + // Expand the tree; this is kept outside of _addObjectToTree() and _processQueue() to allow + // temporarily detaching the store from the tree, which slightly reduces flickering + for (auto path: _paths_to_be_expanded) { + _tree.expand_to_path(path); + _tree.collapse_row(path); + } + + _blockAllSignals(false); + _objectsSelected(_desktop->selection); //Set the tree selection; will also invoke _checkTreeSelection() + _pending_update = false; + return false; // Return false to kill the timeout signal that kept calling _processQueue +} + +/** + * Fills in the details of an item in the already existing row of the tree view + * @param item Item of which the name, visibility, lock status, etc, will be filled in + * @param row Row where the item is residing + * @param expanded True if the item is part of a group that is shown as expanded in the tree view + */ +void ObjectsPanel::_addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &row, bool expanded) +{ + SPGroup * group = SP_IS_GROUP(item) ? SP_GROUP(item) : nullptr; + + row[_model->_colObject] = item; + gchar const * label = item->label() ? item->label() : item->getId(); + row[_model->_colLabel] = label ? label : item->defaultLabel(); + row[_model->_colVisible] = !item->isHidden(); + row[_model->_colLocked] = !item->isSensitive(); + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + row[_model->_colPrevSelectionState] = false; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + //If our parent object is a group and it's expanded, expand the tree + if (expanded) { + _paths_to_be_expanded.emplace_back(_store->get_path(row)); + } +} + +/** + * Updates an item in the tree and optionally recursively updates the item's children + * @param obj The item to update in the tree + * @param recurse Whether to recurse through the item's children + */ +void ObjectsPanel::_updateObject( SPObject *obj, bool recurse ) { + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(SP_ITEM(obj), tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found our item in the tree; now update it! + SPItem * item = SP_IS_ITEM(obj) ? SP_ITEM(obj) : nullptr; + SPGroup * group = SP_IS_GROUP(obj) ? SP_GROUP(obj) : nullptr; + + gchar const * label = obj->label() ? obj->label() : obj->getId(); + row[_model->_colLabel] = label ? label : obj->defaultLabel(); + row[_model->_colVisible] = item ? !item->isHidden() : false; + row[_model->_colLocked] = item ? !item->isSensitive() : false; + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item ? (item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00) : 0; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + if (recurse){ + for (auto& iter: obj->children) { + _updateObject(&iter, recurse); + } + } + } +} + +/** + * Updates the composite controls for the selected item + */ +void ObjectsPanel::_updateComposite() { + if (!_blockCompositeUpdate) + { + //Set the default values + bool setValues = true; + + //Get/set the values + _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_compositingChanged), &setValues)); + + } +} + +/** + * Sets the compositing values for the first selected item in the tree + * @param iter Current tree item + * @param setValues Whether to set the compositing values + */ +void ObjectsPanel::_compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ) +{ + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (*setValues) + { + _setCompositingValues(item); + *setValues = false; + } + } +} + +/** + * Occurs when the current desktop selection changes + * @param sel The current selection + */ +void ObjectsPanel::_objectsSelected( Selection *sel ) { + + bool setOpacity = true; + _selectedConnection.block(); + + _tree.get_selection()->unselect_all(); + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + + SPItem *item = nullptr; + auto items = sel->items(); + for(auto i=items.begin(); i!=items.end(); ++i){ + item = *i; + if (setOpacity) + { + _setCompositingValues(item); + setOpacity = false; + } + _updateObjectSelected(item, (*i)==items.back(), false); + } + if (!item) { + if (_desktop->currentLayer() && SP_IS_ITEM(_desktop->currentLayer())) { + item = SP_ITEM(_desktop->currentLayer()); + _setCompositingValues(item); + _updateObjectSelected(item, false, true); + } + } + _selectedConnection.unblock(); + _checkTreeSelection(); +} + +/** + * Helper function for setting the compositing values + * @param item Item to use for setting the compositing values + */ +void ObjectsPanel::_setCompositingValues(SPItem *item) +{ + // Block the connections to avoid interference + _isolationConnection.block(); + _opacityConnection.block(); + _blendConnection.block(); + _blurConnection.block(); + + // Set the isolation + auto isolation = item->style->isolation.set ? item->style->isolation.value : SP_CSS_ISOLATION_AUTO; + _filter_modifier.set_isolation_mode(isolation, true); + // Set the opacity + double opacity = (item->style->opacity.set ? SP_SCALE24_TO_FLOAT(item->style->opacity.value) : 1); + opacity *= 100; // Display in percent. + _filter_modifier.set_opacity_value(opacity); + // Set the blend mode + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, true); + } else { + _filter_modifier.set_blend_mode(item->style->mix_blend_mode.value, true); + } + if (_filter_modifier.get_blend_mode() == SP_CSS_BLEND_NORMAL) { + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + auto blend = filter_get_legacy_blend(item); + _filter_modifier.set_blend_mode(blend, true); + } + } + SPGaussianBlur *spblur = nullptr; + if (item->style->getFilter()) { + for (auto& primitive_obj: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive_obj) && !spblur) { + //Get the blur value + spblur = SP_GAUSSIANBLUR(&primitive_obj); + } + } + } + + //Set the blur value + double blur_value = 0; + if (spblur) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); // calculating the bbox is expensive; only do this if we have an spblur in the first place + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + blur_value = spblur->stdDeviation.getNumber() * 400 / perimeter; + } + } + _filter_modifier.set_blur_value(blur_value); + + //Unblock connections + _isolationConnection.unblock(); + _blurConnection.unblock(); + _blendConnection.unblock(); + _opacityConnection.unblock(); +} + +// See the comment in objects.h for _tree_cache +/** + * Find the specified item in the tree cache + * @param iter Current tree item + * @param tree_iter Tree_iter will point to the row in which the tree item was found + * @return True if found + */ +bool ObjectsPanel::_findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter) { + if (not item) { + return false; + } + + try { + tree_iter = _tree_cache.at(item); + } + catch (std::out_of_range) { + // Apparently, item cannot be found in the tree_cache, which could mean that + // - the tree and/or tree_cache are out-dated or in the process of being updated. + // - a layer is selected, which is not visible in the objects panel (see _objectsSelected()) + // Anyway, this doesn't seem all that critical, so no warnings; just return false + return false; + } + + /* If the row in the tree has been deleted, and an old tree_cache is being used, then we will + * get a segmentation fault crash somewhere here; so make sure iters don't linger around! + * We can only check the validity as done below, but this is rather slow according to the + * documentation (adds 0.25 s for a 2k long tree). But better safe than sorry + */ + if (not _store->iter_is_valid(tree_iter)) { + g_critical("Invalid iterator to Gtk::tree in objects panel; just prevented a segfault!"); + return false; + } + + return true; +} + + +/** + * Find the specified item in the tree store and (de)select it, optionally scrolling to the item + * @param item Item to select in the tree + * @param scrollto Whether to scroll to the item + * @param expand If true, the path in the tree towards item will be expanded + */ +void ObjectsPanel::_updateObjectSelected(SPItem* item, bool scrollto, bool expand) +{ + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(item, tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found the item! Expand to the path and select it in the tree. + Gtk::TreePath path = _store->get_path(tree_iter); + _tree.expand_to_path( path ); + if (!expand) + // but don't expand itself, just the path + _tree.collapse_row(path); + + Glib::RefPtr select = _tree.get_selection(); + + select->select(tree_iter); + row[_model->_colPrevSelectionState] = true; + if (scrollto) { + //Scroll to the item in the tree + _tree.scroll_to_row(path, 0.5); + } + } +} + +/** + * Pushes the current tree selection to the canvas + */ +void ObjectsPanel::_pushTreeSelectionToCurrent() +{ + if ( _desktop && _desktop->currentRoot() ) { + //block connections for selection and compositing values to prevent interference + _selectionChangedConnection.block(); + _documentChangedCurrentLayer.block(); + //Clear the selection and then iterate over the tree selection, pushing each item to the desktop + _desktop->selection->clear(); + if (_tree.get_selection()->count_selected_rows() == 0) { + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + } + bool setOpacity = true; + bool first_pass = true; + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + first_pass = false; + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + + //unblock connections, unless we were already blocking them beforehand + _selectionChangedConnection.unblock(); + _documentChangedCurrentLayer.unblock(); + + _checkTreeSelection(); + } +} + +/** + * Helper function for pushing the current tree selection to the current desktop + * @param iter Current tree item + * @param setCompositingValues Whether to set the compositing values + */ +bool ObjectsPanel::_selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setCompositingValues, bool *first_pass) +{ + Gtk::TreeModel::Row row = *iter; + bool selected = _tree.get_selection()->is_selected(iter); + if (selected) { // All items selected in the treeview will be added to the current selection + /* Adding/removing only the items that were selected or deselected since the previous call to _pushTreeSelectionToCurrent() + * is very slow on large documents, because _desktop->selection->remove(item) needs to traverse the whole ObjectSet to find + * the item to be removed. When all N objects are selected in a document, clearing the whole selection would require O(N^2) + * That's why we simply clear the complete selection using _desktop->selection->clear(), and re-add all items one by one. + * This is much faster. + */ + + /* On the first pass, we will add only the items that were selected before too. Then, on the second pass, we will add the + * newly selected items such that the last selected items will be actually last. This is needed for example when the user + * wants to align relative to the last selected item. + */ + if (*first_pass == row[_model->_colPrevSelectionState]) { + SPItem *item = row[_model->_colObject]; + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) { + //If the item is not a layer, then select it and set the current layer to its parent (if it's the first item) + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item->parent); + } + _desktop->selection->add(item); + } else { + //If the item is a layer, set the current layer + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item); + } + } + if (*setCompositingValues) { + //Only set the compositing values for the first item <-- TODO: We have this comment here, but this has not actually been implemented? + _setCompositingValues(item); + *setCompositingValues = false; + } + } + } + + if (not *first_pass) { + row[_model->_colPrevSelectionState] = selected; + } + + return false; +} + +bool ObjectsPanel::_clearPrevSelectionState( const Gtk::TreeModel::iterator& iter) { + Gtk::TreeModel::Row row = *iter; + row[_model->_colPrevSelectionState] = false; + SPItem *item = row[_model->_colObject]; + return false; +} + +/** + * Handles button sensitivity + */ +void ObjectsPanel::_checkTreeSelection() +{ + bool sensitive = _tree.get_selection()->count_selected_rows() > 0; + //TODO: top/bottom sensitivity + bool sensitiveNonTop = true; + bool sensitiveNonBottom = true; + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } + + _tree.set_reorderable(sensitive); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected +} + +/** + * Sets visibility of items in the tree + * @param iter Current item in the tree + * @param visible Whether the item should be visible or not + */ +void ObjectsPanel::_setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setHidden( !visible ); + row[_model->_colVisible] = visible; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Sets sensitivity of items in the tree + * @param iter Current item in the tree + * @param locked Whether the item should be locked + */ +void ObjectsPanel::_setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setLocked( locked ); + row[_model->_colLocked] = locked; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Handles keyboard events + * @param event Keyboard event passed in from GDK + * @return Whether the event should be eaten (om nom nom) + */ +bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event) +{ + if (!_desktop) + return false; + + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event(event); + + switch (shortcut) { + // how to get users key binding for the action “start-interactive-search” ?? + // ctrl+f is just the default + case GDK_KEY_f | SP_SHORTCUT_CONTROL_MASK: + return false; + break; + // shall we slurp ctrl+w to close panel? + + // defocus: + case GDK_KEY_Escape: + if (_desktop->canvas) { + gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); + return true; + } + break; + } + + // invoke user defined shortcuts first + bool done = sp_shortcut_invoke(shortcut, _desktop); + if (done) + return true; + + // handle events for the treeview + bool empty = _desktop->selection->isEmpty(); + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn *focus_column = nullptr; + + _tree.get_cursor(path, focus_column); + if (focus_column == _name_column && !_text_renderer->property_editable()) { + //Rename item + _text_renderer->property_editable() = true; + _tree.set_cursor(path, *_name_column, true); + grab_focus(); + return true; + } + return false; + break; + } + } + return false; +} + +/** + * Handles mouse events + * @param event Mouse event from GDK + * @return whether to eat the event (om nom nom) + */ +bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + static bool overVisible = false; + + //Right mouse button was clicked, launch the pop-up menu + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + Gtk::TreeModel::Path path; + int x = static_cast(event->x); + int y = static_cast(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + _popupMenu.popup_at_pointer(reinterpret_cast(event)); + + if (_tree.get_selection()->is_selected(path)) { + return true; + } + } + } + + //Left mouse button was pressed! In order to handle multiple item drag & drop, + //we need to defer selection by setting the select function so that the tree doesn't + //automatically select anything. In order to handle multiple item icon clicking, + //we need to eat the event. There might be a better way to do both of these... + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { + overVisible = false; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1)) { + //Click on visible column, eat this event to keep row selection + overVisible = true; + return true; + } else if (col == _tree.get_column(COL_LOCKED-1) || + col == _tree.get_column(COL_TYPE-1) || + //col == _tree.get_column(COL_INSERTORDER - 1) || + col == _tree.get_column(COL_HIGHLIGHT-1)) { + //Click on an icon column, eat this event to keep row selection + return true; + } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { + //Click on a selected item with no modifiers, defer selection to the mouse-up by + //setting the select function to _noSelection + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_noSelection)); + _defer_target = path; + } + } + } + + //Restore the selection function to allow tree selection on mouse button release + if ( event->type == GDK_BUTTON_RELEASE) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction)); + } + + //CellRenderers do not have good support for dealing with multiple items, so + //we handle all events on them here + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (_defer_target) { + //We had deferred a selection target, select it here (assuming no drag & drop) + if (_defer_target == path && !(event->x == 0 && event->y == 0)) + { + _tree.set_cursor(path, *col, false); + } + _defer_target = Gtk::TreeModel::Path(); + } + else { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } else { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + + if (col == _tree.get_column(COL_VISIBLE - 1)) { + if (overVisible) { + //Toggle visibility + bool newValue = !row[_model->_colVisible]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the visibility + //for all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setVisibleIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its visibility + row[_model->_colVisible] = newValue; + item->setHidden(!newValue); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Unhide objects") : _("Hide objects")); + overVisible = false; + } + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + //Toggle locking + bool newValue = !row[_model->_colLocked]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the sensitivity for + //all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setLockedIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its sensitivity + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Lock objects") : _("Unlock objects")); + + } else if (col == _tree.get_column(COL_TYPE - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item between a group and a layer + SPGroup * g = SP_GROUP(item); + bool newValue = g->layerMode() == SPGroup::LAYER; + row[_model->_colType] = newValue ? 1: 2; + g->setLayerMode(newValue ? SPGroup::GROUP : SPGroup::LAYER); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Layer to group") : _("Group to layer")); + _pushTreeSelectionToCurrent(); + } + } /*else if (col == _tree.get_column(COL_INSERTORDER - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item's insert order + SPGroup * g = SP_GROUP(item); + bool newValue = !g->insertBottom(); + row[_model->_colInsertOrder] = newValue ? 2: 1; + g->setInsertBottom(newValue); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Set insert mode bottom") : _("Set insert mode top")); + } + }*/ else if (col == _tree.get_column(COL_HIGHLIGHT - 1)) { + //Clear the highlight targets + _highlight_target.clear(); + if (_tree.get_selection()->is_selected(path)) + { + //If the current item is selected, store all selected items + //in the highlight source + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeHighlightTarget)); + } else { + //If the current item is not selected, store only it in the highlight source + _storeHighlightTarget(iter); + } + if (_selectedColor) + { + //Set up the color selector + SPColor color; + color.set( row[_model->_colHighlight] ); + _selectedColor->setColorAlpha(color, SP_RGBA32_A_F(row[_model->_colHighlight])); + } + //Show the color selector dialog + _colorSelectorDialog.show(); + } + } + } + } + } + + //Second mouse button press, set double click status for when the mouse is released + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + //Double click on mouse button release, if we're over the label column, edit + //the item name + if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { + doubleclick = 0; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { + // Double click on the Layer name, enable editing + _text_renderer->property_editable() = true; + _tree.set_cursor (path, *_name_column, true); + grab_focus(); + } + } + + return false; +} + +/** + * Stores items in the highlight target vector to manipulate with the color selector + * @param iter Current tree item to store + */ +void ObjectsPanel::_storeHighlightTarget(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + _highlight_target.push_back(item); + } +} + +/* + * Drag and drop within the tree + */ +bool ObjectsPanel::_handleDragDrop(const Glib::RefPtr& /*context*/, int x, int y, guint /*time*/) +{ + //Set up our defaults and clear the source vector + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source.clear(); + _dnd_source_includes_layer = false; + + //Add all selected items to the source vector + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeDragSource)); + + bool cancel_dnd = false; + bool dnd_to_top_at_end = false; + + Gtk::TreeModel::Path target_path; + Gtk::TreeViewDropPosition pos; + if (_tree.get_dest_row_at_pos(x, y, target_path, pos)) { + // SPItem::moveTo() will be used to move the selected items to their new position, but + // moveTo() can only "drop before"; we therefore need to find the next path and drop + // the selection just before it, instead of "dropping after" the target path + if (pos == Gtk::TREE_VIEW_DROP_AFTER) { + Gtk::TreeModel::Path next_path = target_path; + if (_tree.row_expanded(next_path)) { + next_path.down(); // The next path is at a lower level in the hierarchy, i.e. in a layer or group + } else { + next_path.next(); // The next path is at the same level + } + // A next path might however not be present, if we're dropping at the end of the tree view + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" of the treeview ; we'll get the parent group or layer of the last + // item, and drop into that parent + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into the parent of the last item + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level, completely at the end of the treeview; + dnd_to_top_at_end = true; + } + } + } + + if (dnd_to_top_at_end) { + g_assert(_dnd_target == nullptr); + } else { + // Find the SPItem corresponding to the target_path/row at which we're dropping our selection + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + _dnd_target = row[_model->_colObject]; //Set the drop target + if ((pos == Gtk::TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == Gtk::TREE_VIEW_DROP_INTO_OR_AFTER)) { + // Trying to drop into a layer or group + if (SP_IS_GROUP(_dnd_target)) { + _dnd_into = true; + } else { + // If the target is not a group (or layer), then we cannot drop into it (unless we + // would create a group on the fly), so we will cancel the drag and drop action. + cancel_dnd = true; + } + } + // If the source selection contains a layer however, then it can not be dropped ... + bool c1 = target_path.size() > 1; // .. below the top-level + bool c2 = SP_IS_GROUP(_dnd_target) and _dnd_into; // .. or in any group (at the top level) + if (_dnd_source_includes_layer and (c1 or c2)) { + cancel_dnd = true; + } + } else { + cancel_dnd = true; + } + } + } + + if (not cancel_dnd) { + _takeAction(DRAGNDROP); + } + + return true; // If True: then we're signaling here that nothing needs to be done by the TreeView; we're updating ourselves.. +} + +/** + * Stores all selected items as the drag source + * @param iter Current tree item + */ +void ObjectsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) { + _dnd_source.push_back(item); + if (SP_IS_GROUP(item) && (SP_GROUP(item)->layerMode() == SPGroup::LAYER)) { + _dnd_source_includes_layer = true; + } + } +} + +/* + * Move a selection of items in response to a drag & drop action + */ +void ObjectsPanel::_doTreeMove( ) +{ + g_assert(_desktop != nullptr); + g_assert(_document != nullptr); + + std::vector idvector; + + //Clear the desktop selection + _desktop->selection->clear(); + while (!_dnd_source.empty()) + { + SPItem *obj = _dnd_source.back(); + _dnd_source.pop_back(); + + if (obj != _dnd_target) { + //Store the object id (for selection later) and move the object + idvector.push_back(g_strdup(obj->getId())); + obj->moveTo(_dnd_target, _dnd_into); + } + } + //Select items + while (!idvector.empty()) { + //Grab the id from the vector, get the item in the document and select it + gchar * id = idvector.back(); + idvector.pop_back(); + SPObject *obj = _document->getObjectById(id); + g_free(id); + if (obj && SP_IS_ITEM(obj)) { + SPItem *item = SP_ITEM(obj); + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); + _desktop->selection->add(item); + } + else + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item); + } + } + } + + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Moved objects")); +} + +/** + * Prevents the treeview from emiting and responding to most signals; needed when it's not up to date + */ +void ObjectsPanel::_blockAllSignals(bool should_block = true) { + + // incoming signals + _documentChangedCurrentLayer.block(should_block); + _isolationConnection.block(should_block); + _opacityConnection.block(should_block); + _blendConnection.block(should_block); + _blurConnection.block(should_block); + if (_pending && should_block) { + // Kill any pending UI event, e.g. a delete or drag 'n drop action, which could + // become unpredictable after the tree has been updated + _pending->_signal.disconnect(); + } + + _selectionChangedConnection.block(should_block); + // outgoing signal + _selectedConnection.block(should_block); + + // These are not blocked: desktopChangeConn, _documentChangedConnection +} + +/** + * Fires the action verb + */ +void ObjectsPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(_desktop); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +bool ObjectsPanel::_executeUpdate() { + _objectsChanged(nullptr); + return false; +} + +/** + * Executes the given button action during the idle time + */ +void ObjectsPanel::_takeAction( int val ) +{ + if (val == UPDATE_TREE) { + _pending_update = true; + // We might already have been updating the tree, but new data is available now + // so we will then first cancel the old update before scheduling a new one + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _blockAllSignals(true); + //_store->clear(); + _tree_cache.clear(); + _executeUpdate_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeUpdate), 500, Glib::PRIORITY_DEFAULT_IDLE+50); + // In the spray tool, updating the tree competes in priority with the redrawing of the canvas, + // see SPCanvas::addIdle(), which is set to UPDATE_PRIORITY (=G_PRIORITY_DEFAULT_IDLE). We + // should take a lower priority (= higher value) to keep the spray tool updating longer, and to prevent + // the objects-panel from clogging the processor; however, once the spraying slows down, the tree might + // get updated anyway. + } else if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_signal = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeAction), 0 ); + } +} + +/** + * Executes the pending button action + */ +bool ObjectsPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _document && _pending) + { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_FRONT); + } + } + break; + case BUTTON_BOTTOM: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_BACK); + } + } + break; + case BUTTON_UP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_UP ); + } + } + break; + case BUTTON_DOWN: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_DOWN ); + } + } + break; + case BUTTON_DUPLICATE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + else + { + _fireAction( SP_VERB_EDIT_DUPLICATE ); + } + } + break; + case BUTTON_DELETE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + else + { + _fireAction( SP_VERB_EDIT_DELETE ); + } + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case BUTTON_CLIPGROUP: + { + _fireAction ( SP_VERB_OBJECT_CREATE_CLIP_GROUP ); + } + case BUTTON_SETCLIP: + { + _fireAction( SP_VERB_OBJECT_SET_CLIPPATH ); + } + break; + case BUTTON_UNSETCLIP: + { + _fireAction( SP_VERB_OBJECT_UNSET_CLIPPATH ); + } + break; + case BUTTON_SETMASK: + { + _fireAction( SP_VERB_OBJECT_SET_MASK ); + } + break; + case BUTTON_UNSETMASK: + { + _fireAction( SP_VERB_OBJECT_UNSET_MASK ); + } + break; + case BUTTON_GROUP: + { + _fireAction( SP_VERB_SELECTION_GROUP ); + } + break; + case BUTTON_UNGROUP: + { + _fireAction( SP_VERB_SELECTION_UNGROUP ); + } + break; + case BUTTON_COLLAPSE_ALL: + { + for (auto& obj: _document->getRoot()->children) { + if (SP_IS_GROUP(&obj)) { + _setCollapsed(SP_GROUP(&obj)); + } + } + _objectsChanged(_document->getRoot()); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + // The notifyChildOrderChanged signal will ensure that the TreeView gets updated + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +/** + * Handles an unsuccessful item label edit (escape pressed, etc.) + */ +void ObjectsPanel::_handleEditingCancelled() +{ + _text_renderer->property_editable() = false; +} + +/** + * Handle a successful item label edit + * @param path Tree path of the item currently being edited + * @param new_text New label text + */ +void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameObject(row, new_text); + _text_renderer->property_editable() = false; +} + +/** + * Renames an item in the tree + * @param row Tree row + * @param name New label to give to the item + */ +void ObjectsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop) { + SPItem* item = row[_model->_colObject]; + if ( item ) { + gchar const* oldLabel = item->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + item->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } + } +} + +/** + * A row selection function used by the tree that doesn't allow any new items to be selected. + * Currently, this is used to allow multi-item drag & drop. + */ +bool ObjectsPanel::_noSelection( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) +{ + return false; +} + +/** + * Default row selection function taken from the layers dialog + */ +bool ObjectsPanel::_rowSelectFunction( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Sets a group to be collapsed and recursively collapses its children + * @param group The group to collapse + */ +void ObjectsPanel::_setCollapsed(SPGroup * group) +{ + group->setExpanded(false); + group->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + for (auto& iter: group->children) { + if (SP_IS_GROUP(&iter)) { + _setCollapsed(SP_GROUP(&iter)); + } + } +} + +/** + * Sets a group to be expanded or collapsed + * @param iter Current tree item + * @param isexpanded Whether to expand or collapse + */ +void ObjectsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) +{ + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + if (item && SP_IS_GROUP(item)) + { + if (isexpanded) + { + //If we're expanding, simply perform the expansion + SP_GROUP(item)->setExpanded(isexpanded); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + else + { + //If we're collapsing, we need to recursively collapse, so call our helper function + _setCollapsed(SP_GROUP(item)); + } + } +} + +/** + * Callback for when the highlight color is changed + * @param csel Color selector + * @param cp Objects panel + */ +void ObjectsPanel::_highlightPickerColorMod() +{ + SPColor color; + float alpha = 0; + _selectedColor->colorAlpha(color, alpha); + + guint32 rgba = color.toRGBA32( alpha ); + + //Set the highlight color for all items in the _highlight_target (all selected items) + for (auto target : _highlight_target) + { + target->setHighlightColor(rgba); + target->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::maybeDone(SP_ACTIVE_DOCUMENT, "highlight", SP_VERB_DIALOG_OBJECTS, _("Set object highlight color")); +} + +/** + * Callback for when the opacity value is changed + */ +void ObjectsPanel::_opacityValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_opacityChangedIter)); + DocumentUndo::maybeDone(_document, "opacity", SP_VERB_DIALOG_OBJECTS, _("Set object opacity")); + _blockCompositeUpdate = false; +} + +/** + * Change the opacity of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_opacityChangedIter(const Gtk::TreeIter& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->style->opacity.set = TRUE; + item->style->opacity.value = SP_SCALE24_FROM_FLOAT(_filter_modifier.get_opacity_value() / 100); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the isolation value is changed + */ +void ObjectsPanel::_isolationValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_isolationChangedIter)); + DocumentUndo::maybeDone(_document, "isolation", SP_VERB_DIALOG_OBJECTS, _("Set object isolation")); + _blockCompositeUpdate = false; +} + +/** + * Change the isolation of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_isolationChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (item) { + item->style->isolation.set = TRUE; + item->style->isolation.value = _filter_modifier.get_isolation_mode(); + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.set = TRUE; + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blend mode is changed + */ +void ObjectsPanel::_blendValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_blendChangedIter)); + DocumentUndo::done(_document, SP_VERB_DIALOG_OBJECTS, _("Set object blend mode")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blend mode of the selected tree items + * @param iter Current tree item + * @param blendmode Blend mode to set + */ +void ObjectsPanel::_blendChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + // < 1.0 filter based blend removal + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + remove_filter_legacy_blend(item); + } + item->style->mix_blend_mode.set = TRUE; + if (_filter_modifier.get_blend_mode() && + item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) + { + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } else { + item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blur value has changed + */ +void ObjectsPanel::_blurValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_blurChangedIter), _filter_modifier.get_blur_value())); + DocumentUndo::maybeDone(_document, "blur", SP_VERB_DIALOG_OBJECTS, _("Set object blur")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blur value for the selected items in the tree + * @param iter Current tree item + * @param blur Blur value to set + */ +void ObjectsPanel::_blurChangedIter(const Gtk::TreeIter& iter, double blur) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + //Since blur and blend are both filters, we need to set both at the same time + SPStyle *style = item->style; + if (style) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); + double radius; + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + radius = blur * perimeter / 400; + } else { + radius = 0; + } + + if (radius != 0) { + // The modify function expects radius to be in display pixels. + Geom::Affine i2d (item->i2dt_affine()); + double expansion = i2d.descrim(); + radius *= expansion; + SPFilter *filter = modify_filter_gaussian_blur_from_item(_document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } else if (item->style->filter.set && item->style->getFilter()) { + for (auto& primitive: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive)) { + primitive.deleteObject(); + break; + } + } + if (!item->style->getFilter()->firstChild()) { + remove_filter(item, false); + } + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } +} + +/** + * Constructor + */ +ObjectsPanel::ObjectsPanel() : + UI::Widget::Panel("/dialogs/objects", SP_VERB_DIALOG_OBJECTS), + _rootWatcher(nullptr), + _deskTrack(), + _desktop(nullptr), + _document(nullptr), + _model(nullptr), + _pending(nullptr), + _pending_update(false), + _toggleEvent(nullptr), + _defer_target(), + _visibleHeader(C_("Visibility", "V")), + _lockHeader(C_("Lock", "L")), + _typeHeader(C_("Type", "T")), + _clipmaskHeader(C_("Clip and mask", "CM")), + _highlightHeader(C_("Highlight", "HL")), + _nameHeader(_("Label")), + _filter_modifier( UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY ), + _colorSelectorDialog("dialogs.colorpickerwindow") +{ + //Create the tree model and store + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + //Set up the tree + _tree.set_model( _store ); + _tree.set_headers_visible(true); + _tree.set_reorderable(false); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + //Create the column CellRenderers + //Visible + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + // In order to get tooltips on header, we must create our own label. + _visibleHeader.set_tooltip_text(_("Toggle visibility of Layer, Group, or Object.")); + _visibleHeader.show(); + col->set_widget( _visibleHeader ); + } + + //Locked + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + _lockHeader.set_tooltip_text(_("Toggle lock of Layer, Group, or Object.")); + _lockHeader.show(); + col->set_widget( _lockHeader ); + } + + //Type + Inkscape::UI::Widget::LayerTypeIcon * typeRenderer = Gtk::manage( new Inkscape::UI::Widget::LayerTypeIcon()); + int typeColNum = _tree.append_column("type", *typeRenderer) - 1; + typeRenderer->property_activatable() = true; + col = _tree.get_column(typeColNum); + if ( col ) { + col->add_attribute( typeRenderer->property_active(), _model->_colType ); + _typeHeader.set_tooltip_text(_("Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles between the two types.")); + _typeHeader.show(); + col->set_widget( _typeHeader ); + } + + //Insert order (LiamW: unused) + /*Inkscape::UI::Widget::InsertOrderIcon * insertRenderer = Gtk::manage( new Inkscape::UI::Widget::InsertOrderIcon()); + int insertColNum = _tree.append_column("type", *insertRenderer) - 1; + col = _tree.get_column(insertColNum); + if ( col ) { + col->add_attribute( insertRenderer->property_active(), _model->_colInsertOrder ); + }*/ + + //Clip/mask + Inkscape::UI::Widget::ClipMaskIcon * clipRenderer = Gtk::manage( new Inkscape::UI::Widget::ClipMaskIcon()); + int clipColNum = _tree.append_column("clipmask", *clipRenderer) - 1; + col = _tree.get_column(clipColNum); + if ( col ) { + col->add_attribute( clipRenderer->property_active(), _model->_colClipMask ); + _clipmaskHeader.set_tooltip_text(_("Is object clipped and/or masked?")); + _clipmaskHeader.show(); + col->set_widget( _clipmaskHeader ); + } + + //Highlight + Inkscape::UI::Widget::HighlightPicker * highlightRenderer = Gtk::manage( new Inkscape::UI::Widget::HighlightPicker()); + int highlightColNum = _tree.append_column("highlight", *highlightRenderer) - 1; + col = _tree.get_column(highlightColNum); + if ( col ) { + col->add_attribute( highlightRenderer->property_active(), _model->_colHighlight ); + _highlightHeader.set_tooltip_text(_("Highlight color of outline in Node tool. Click to set. If alpha is zero, use inherited color.")); + _highlightHeader.show(); + col->set_widget( _highlightHeader ); + } + + //Label + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + if( _name_column ) { + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + _nameHeader.set_tooltip_text(_("Layer/Group/Object label (inkscape:label). Double-click to set. Default value is object 'id'.")); + _nameHeader.show(); + _name_column->set_widget( _nameHeader ); + } + + //Set the expander and search columns + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(_model->_colLabel); + // use ctrl+f to start search + _tree.set_enable_search(false); + + //Set up the tree selection + _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction) ); + + //Set up tree signals + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false ); + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleDragDrop), false); + _tree.signal_row_collapsed().connect( sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false)); + _tree.signal_row_expanded().connect( sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), true)); + + //Set up the label editing signals + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEdited) ); + _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEditingCancelled) ); + + //Set up the scroller window and pack the page + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _page.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + + //Set up the compositing items + _blendConnection = _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blendValueChanged)); + _blurConnection = _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blurValueChanged)); + _opacityConnection = _filter_modifier.signal_opacity_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_opacityValueChanged)); + _isolationConnection = _filter_modifier.signal_isolation_changed().connect( + sigc::mem_fun(*this, &ObjectsPanel::_isolationValueChanged)); + //Pack the compositing functions and the button row + _page.pack_end(_filter_modifier, Gtk::PACK_SHRINK); + _page.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + //Pack into the panel contents + _getContents()->pack_start(_page, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + //Set up the button row + + + //Add object/layer + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-add"), _("Add layer...")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Remove object + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-remove"), _("Remove object")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Move to bottom + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-bottom"), _("Move To Bottom")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move down + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-down"), _("Move Down")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move up + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-up"), _("Move Up")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move to top + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-top"), _("Move To Top")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Collapse all + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("format-indent-less"), _("Collapse All")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_COLLAPSE_ALL) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + _watching.push_back(&_filter_modifier); + + //Set up the pop-up menu + // ------------------------------------------------------- + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_objects", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_UP, (int)BUTTON_UP) ); + _watchingNonBottom.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_DOWN, (int)BUTTON_DOWN) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_GROUP, (int)BUTTON_GROUP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_UNGROUP, (int)BUTTON_UNGROUP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_CLIPPATH, (int)BUTTON_SETCLIP ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_CREATE_CLIP_GROUP, (int)BUTTON_CLIPGROUP ) ); + + //will never be implemented + //_watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, (int)BUTTON_SETINVCLIP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_CLIPPATH, (int)BUTTON_UNSETCLIP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_MASK, (int)BUTTON_SETMASK ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_MASK, (int)BUTTON_UNSETMASK ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + //Set initial sensitivity of buttons + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + //Set up the color selection dialog + GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); + sp_transientize(dlg); + + _colorSelectorDialog.hide(); + _colorSelectorDialog.set_title (_("Select Highlight Color")); + _colorSelectorDialog.set_border_width (4); + _colorSelectorDialog.property_modal() = true; + _selectedColor.reset(new Inkscape::UI::SelectedColor); + Gtk::Widget *color_selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(*_selectedColor)); + _colorSelectorDialog.get_content_area()->pack_start ( + *color_selector, true, true, 0); + + _selectedColor->signal_dragged.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_released.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_changed.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + + color_selector->show(); + + setDesktop( targetDesktop ); + + show_all_children(); + + //Connect the desktop changed connection + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectsPanel::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); +} + +/** + * Callback method that will be called when the desktop is destroyed + */ +void ObjectsPanel::_desktopDestroyed(SPDesktop* /*desktop*/) { + // We need to make sure that we're not trying to update the tree after the desktop has vanished, e.g. + // when closing Inkscape. Preferably, we would have done so in the destructor of the ObjectsPanel. But + // as this destructor is never ever called, we will do this by attaching to the desktop_destroyed signal + // instead + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _desktop = nullptr; +} + +/** + * Destructor + */ +ObjectsPanel::~ObjectsPanel() +{ + // Never being called, not even when closing Inkscape? + + //Close the highlight selection dialog + _colorSelectorDialog.hide(); + + //Set the desktop to null, which will disconnect all object watchers + setDesktop(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +/** + * Sets the current document + */ +void ObjectsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) +{ + //Clear all object watchers + _removeWatchers(); + + //Delete the root watcher + if (_rootWatcher) + { + _rootWatcher->_repr->removeObserver(*_rootWatcher); + delete _rootWatcher; + _rootWatcher = nullptr; + } + + _document = document; + + if (document && document->getRoot() && document->getRoot()->getRepr()) + { + //Create a new root watcher for the document and then call _objectsChanged to fill the tree + _rootWatcher = new ObjectsPanel::ObjectWatcher(this, document->getRoot()); + document->getRoot()->getRepr()->addObserver(*_rootWatcher); + _objectsChanged(document->getRoot()); + } +} + +/** + * Set the current panel desktop + */ +void ObjectsPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _documentChangedConnection.disconnect(); + _documentChangedCurrentLayer.disconnect(); + _selectionChangedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //Connect desktop signals + _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &ObjectsPanel::setDocument)); + + _documentChangedCurrentLayer = _desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsChangedWrapper)); + + _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsSelected)); + + _desktopDestroyedConnection = _desktop->connectDestroy( sigc::mem_fun(*this, &ObjectsPanel::_desktopDestroyed)); + + setDocument(_desktop, _desktop->doc()); + } else { + setDocument(nullptr, nullptr); + } + } + _deskTrack.setBase(desktop); +} +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +//should be okay to put these here because they are never referenced anywhere else +using namespace Inkscape::UI::Tools; + +void SPItem::setHighlightColor(guint32 const color) +{ + g_free(_highlightColor); + if (color & 0x000000ff) + { + _highlightColor = g_strdup_printf("%u", color); + } + else + { + _highlightColor = nullptr; + } + + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +void SPItem::unsetHighlightColor() +{ + g_free(_highlightColor); + _highlightColor = nullptr; + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +/* + 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 : diff --git a/src/ui/dialog/objects.h b/src/ui/dialog/objects.h new file mode 100644 index 0000000..dff1498 --- /dev/null +++ b/src/ui/dialog/objects.h @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for objects UI. + * + * Authors: + * Theodore Janeczko + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 + * Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_OBJECTS_PANEL_H +#define SEEN_OBJECTS_PANEL_H + +#include +#include +#include +#include +#include +#include +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" +#include "selection.h" +#include "ui/widget/filter-effect-chooser.h" + +class SPObject; +class SPGroup; +struct SPColorSelector; + +namespace Inkscape { + +namespace UI { + +class SelectedColor; + +namespace Dialog { + + +/** + * A panel that displays objects. + */ +class ObjectsPanel : public UI::Widget::Panel +{ +public: + ObjectsPanel(); + ~ObjectsPanel() override; + + static ObjectsPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void setDocument( SPDesktop* desktop, SPDocument* document); + +private: + //Internal Classes: + class ModelColumns; + class InternalUIBounce; + class ObjectWatcher; + + //Connections, Watchers, Trackers: + + //Document root watcher + ObjectsPanel::ObjectWatcher* _rootWatcher; + + //All object watchers + std::map > _objectWatchers; + + //Connection for when the desktop changes + sigc::connection desktopChangeConn; + + //Connection for when the desktop is destroyed (I.e. its deconstructor is called) + sigc::connection _desktopDestroyedConnection; + + //Connection for when the document changes + sigc::connection _documentChangedConnection; + + //Connection for when the active layer changes + sigc::connection _documentChangedCurrentLayer; + + //Connection for when the active selection in the document changes + sigc::connection _selectionChangedConnection; + + //Connection for when the selection in the dialog changes + sigc::connection _selectedConnection; + + //Connections for when the opacity/blend/blur of the active selection in the document changes + sigc::connection _isolationConnection; + sigc::connection _opacityConnection; + sigc::connection _blendConnection; + sigc::connection _blurConnection; + + sigc::connection _processQueue_sig; + sigc::connection _executeUpdate_sig; + + //Desktop tracker for grabbing the desktop changed connection + DesktopTracker _deskTrack; + + //Members: + + //The current desktop + SPDesktop* _desktop; + + //The current document + SPDocument* _document; + + //Tree data model + ModelColumns* _model; + + //Prevents the composite controls from updating + bool _blockCompositeUpdate; + + // + InternalUIBounce* _pending; + bool _pending_update; + + //Whether the drag & drop was dragged into an item + gboolean _dnd_into; + + //List of drag & drop source items + std::vector _dnd_source; + + //Drag & drop target item + SPItem* _dnd_target; + + // Whether the drag sources include a layer + bool _dnd_source_includes_layer; + + //List of items to change the highlight on + std::vector _highlight_target; + + //Show icons in the context menu + bool _show_contextmenu_icons; + + //GUI Members: + + GdkEvent* _toggleEvent; + + Gtk::TreeModel::Path _defer_target; + + Glib::RefPtr _store; + std::list > _tree_update_queue; + //When the user selects an item in the document, we need to find that item in the tree view + //and highlight it. When looking up a specific item in the tree though, we don't want to have + //to iterate through the whole list, as this would take too long if the list is very long. So + //we will use a std::map for this instead, which is much faster (and call it _tree_cache). It + //would have been cleaner to create our own custom tree model, as described here + //https://en.wikibooks.org/wiki/GTK%2B_By_Example/Tree_View/Tree_Models + std::map _tree_cache; + std::list _selected_objects_order; // ordered by time of selection + std::list _paths_to_be_expanded; + + std::vector _watching; + std::vector _watchingNonTop; + std::vector _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _page; + + Gtk::Label _visibleHeader; + Gtk::Label _lockHeader; + Gtk::Label _typeHeader; + Gtk::Label _clipmaskHeader; + Gtk::Label _highlightHeader; + Gtk::Label _nameHeader; + + /* Composite Settings (blend, blur, opacity). */ + Inkscape::UI::Widget::SimpleFilterModifier _filter_modifier; + + Gtk::Dialog _colorSelectorDialog; + std::unique_ptr _selectedColor; + + //Methods: + + ObjectsPanel(ObjectsPanel const &) = delete; // no copy + ObjectsPanel &operator=(ObjectsPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, char const* iconName, char const* tooltip ); + void _fireAction( unsigned int code ); + + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ); + void _setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + + void _storeHighlightTarget(const Gtk::TreeModel::iterator& iter); + void _storeDragSource(const Gtk::TreeModel::iterator& iter); + bool _handleDragDrop(const Glib::RefPtr& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + bool _selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setOpacity, bool *first_pass); + bool _clearPrevSelectionState(const Gtk::TreeModel::iterator& iter); + void _desktopDestroyed(SPDesktop* desktop); + + void _checkTreeSelection(); + + void _blockAllSignals(bool should_block); + void _takeAction( int val ); + bool _executeAction(); + bool _executeUpdate(); + + void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded ); + void _setCollapsed(SPGroup * group); + + bool _noSelection( Glib::RefPtr const & model, Gtk::TreeModel::Path const & path, bool b ); + bool _rowSelectFunction( Glib::RefPtr const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ); + void _updateComposite(); + void _setCompositingValues(SPItem *item); + + bool _findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter); + void _updateObject(SPObject *obj, bool recurse); + + void _objectsSelected(Selection *sel); + void _updateObjectSelected(SPItem* item, bool scrollto, bool expand); + + void _removeWatchers(bool only_unused); + void _addWatcher(SPItem* item); + void _objectsChangedWrapper(SPObject *obj); + void _objectsChanged(SPObject *obj); + bool _processQueue(); + void _queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow); + void _addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &parentRow, bool expanded); + + void _isolationChangedIter(const Gtk::TreeIter &iter); + void _isolationValueChanged(); + + void _opacityChangedIter(const Gtk::TreeIter& iter); + void _opacityValueChanged(); + + void _blendChangedIter(const Gtk::TreeIter &iter); + void _blendValueChanged(); + + void _blurChangedIter(const Gtk::TreeIter& iter, double blur); + void _blurValueChanged(); + + void _highlightPickerColorMod(); +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_OBJECTS_PANEL_H + +/* + 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 : diff --git a/src/ui/dialog/paint-servers.cpp b/src/ui/dialog/paint-servers.cpp new file mode 100644 index 0000000..ec49312 --- /dev/null +++ b/src/ui/dialog/paint-servers.cpp @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "document.h" +#include "inkscape.h" +#include "paint-servers.h" +#include "path-prefix.h" +#include "style.h" +#include "verbs.h" + +#include "io/resource.h" +#include "object/sp-defs.h" +#include "object/sp-hatch.h" +#include "object/sp-pattern.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "ui/cache/svg_preview_cache.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static Glib::ustring const wrapper = R"=====( + + + + + +)====="; + +class PaintServersColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn id; + Gtk::TreeModelColumn paint; + Gtk::TreeModelColumn> pixbuf; + Gtk::TreeModelColumn document; + + PaintServersColumns() { + add(id); + add(paint); + add(pixbuf); + add(document); + } +}; + +PaintServersColumns *PaintServersDialog::getColumns() { return new PaintServersColumns(); } + +// Constructor +PaintServersDialog::PaintServersDialog(gchar const *prefsPath) + : Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_PAINT) + , desktop(SP_ACTIVE_DESKTOP) + , target_selected(true) + , ALLDOCS(_("All paint servers")) + , CURRENTDOC(_("Current document")) +{ + current_store = ALLDOCS; + + store[ALLDOCS] = Gtk::ListStore::create(*getColumns()); + store[CURRENTDOC] = Gtk::ListStore::create(*getColumns()); + + // Grid holding the contents + Gtk::Grid *grid = Gtk::manage(new Gtk::Grid()); + grid->set_margin_start(3); + grid->set_margin_end(3); + grid->set_margin_top(3); + grid->set_row_spacing(3); + _getContents()->pack_start(*grid, Gtk::PACK_EXPAND_WIDGET); + + // Grid row 0 + Gtk::Label *file_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Server")) + ": ")); + grid->attach(*file_label, 0, 0, 1, 1); + + dropdown = Gtk::manage(new Gtk::ComboBoxText()); + dropdown->append(ALLDOCS); + dropdown->append(CURRENTDOC); + document_map[CURRENTDOC] = desktop->getDocument(); + dropdown->set_active_text(ALLDOCS); + dropdown->set_hexpand(); + grid->attach(*dropdown, 1, 0, 1, 1); + + // Grid row 1 + Gtk::Label *fill_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Change")) + ": ")); + grid->attach(*fill_label, 0, 1, 1, 1); + + target_dropdown = Gtk::manage(new Gtk::ComboBoxText()); + target_dropdown->append(_("Fill")); + target_dropdown->append(_("Stroke")); + target_dropdown->set_active_text(_("Fill")); + target_dropdown->set_hexpand(); + grid->attach(*target_dropdown, 1, 1, 1, 1); + + // Grid row 2 + icon_view = Gtk::manage(new Gtk::IconView( + static_cast>(store[current_store]) + )); + icon_view->set_tooltip_column(0); + icon_view->set_pixbuf_column(2); + icon_view->set_size_request(200, 300); + icon_view->show_all_children(); + icon_view->set_selection_mode(Gtk::SELECTION_SINGLE); + icon_view->set_activate_on_single_click(true); + + Gtk::ScrolledWindow *scroller = Gtk::manage(new Gtk::ScrolledWindow()); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->set_hexpand(); + scroller->set_vexpand(); + scroller->add(*icon_view); + grid->attach(*scroller, 0, 2, 2, 1); + + // Events + target_dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_target_changed) + ); + + dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_document_changed) + ); + + icon_view->signal_item_activated().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_item_activated) + ); + + desktop->getDocument()->getDefs()->connectModified( + sigc::mem_fun(*this, &PaintServersDialog::load_current_document) + ); + + // Get wrapper document (rectangle to fill with paint server). + preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true); + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + if (!rect || !defs) { + std::cerr << "PaintServersDialog::PaintServersDialog: Failed to get wrapper defs or rectangle!!" << std::endl; + } + + // Set up preview document. + unsigned key = SPItem::display_key_new(1); + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY)); + + // Load paint servers from resource files + load_sources(); +} + +PaintServersDialog::~PaintServersDialog() = default; + +// Get url or color value. +Glib::ustring get_url(Glib::ustring paint) +{ + + Glib::MatchInfo matchInfo; + + // Paint server + static Glib::RefPtr regex1 = Glib::Regex::create(":(url\\(#([A-z0-9\\-_\\.#])*\\))"); + regex1->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + // Color + static Glib::RefPtr regex2 = Glib::Regex::create(":(([A-z0-9#])*)"); + regex2->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + return Glib::ustring(); +} + +// This is too complicated to use selectors! +void recurse_find_paint(SPObject* in, std::vector& list) +{ + + g_return_if_fail(in != nullptr); + + // Add paint servers in section. + if (dynamic_cast(in)) { + if (in->getId()) { + // Need to check as one can't construct Glib::ustring with nullptr. + list.push_back (Glib::ustring("url(#") + in->getId() + ")"); + } + // Don't recurse into paint servers. + return; + } + + // Add paint servers referenced by shapes. + if (dynamic_cast(in)) { + list.push_back (get_url(in->style->fill.write())); + list.push_back (get_url(in->style->stroke.write())); + } + + for (auto child: in->childList(false)) { + recurse_find_paint(child, list); + } +} + +// Load paint servers from all the files associated +void PaintServersDialog::load_sources() +{ + + // Extract paints from the current file + load_document(desktop->getDocument()); + + // Extract out paints from files in share/paint. + for (auto &path : get_filenames(Inkscape::IO::Resource::PAINT, { ".svg" })) { + SPDocument *document = SPDocument::createNewDoc(path.c_str(), FALSE); + + load_document(document); + } +} + +Glib::RefPtr PaintServersDialog::get_pixbuf(SPDocument *document, Glib::ustring paint, Glib::ustring *id) +{ + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + + Glib::RefPtr pixbuf(nullptr); + if (paint.empty()) { + return pixbuf; + } + + // Set style on wrapper + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", paint.c_str()); + rect->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + + // Insert paint into defs if required + Glib::MatchInfo matchInfo; + static Glib::RefPtr regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)"); + regex->match(paint, matchInfo); + if (matchInfo.matches()) { + *id = matchInfo.fetch(1); + + // Delete old paint if necessary + std::vector old_paints = preview_document->getObjectsBySelector("defs > *"); + for (auto paint : old_paints) { + paint->deleteObject(false); + } + + // Add new paint + SPObject *new_paint = document->getObjectById(*id); + if (!new_paint) { + std::cerr << "PaintServersDialog::load_document: cannot find paint server: " << id << std::endl; + return pixbuf; + } + + // Create a copy repr of the paint + Inkscape::XML::Document *xml_doc = preview_document->getReprDoc(); + Inkscape::XML::Node *repr = new_paint->getRepr()->duplicate(xml_doc); + defs->appendChild(repr); + } else { + // Temporary block solid color fills. + return pixbuf; + } + + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + Geom::OptRect dbox = dynamic_cast(rect)->visualBounds(); + + if (!dbox) { + return pixbuf; + } + + double size = std::max(dbox->width(), dbox->height()); + + pixbuf = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size)); + + return pixbuf; +} + +// Load paint server from the given document +void PaintServersDialog::load_document(SPDocument *document) +{ + PaintServersColumns *columns = getColumns(); + Glib::ustring document_title; + if (!document->getRoot()->title()) { + document_title = CURRENTDOC; + } else { + document_title = Glib::ustring(document->getRoot()->title()); + } + bool has_server_elements = false; + + // Find all paints + std::vector paints; + recurse_find_paint(document->getRoot(), paints); + + // Sort and remove duplicates. + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + if (paints.size() && store.find(document_title) == store.end()) { + store[document_title] = Gtk::ListStore::create(*getColumns()); + } + + // iterating though servers + for (auto paint : paints) { + Glib::RefPtr pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + // Save as a ListStore column + Gtk::ListStore::iterator iter = store[ALLDOCS]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + + iter = store[document_title]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + has_server_elements = true; + } + + if (has_server_elements && document_map.find(document_title) == document_map.end()) { + document_map[document_title] = document; + dropdown->append(document_title); + } +} + +void PaintServersDialog::load_current_document(SPObject * /*object*/, guint /*flags*/) +{ + std::unique_ptr columns(getColumns()); + SPDocument *document = desktop->getDocument(); + Glib::RefPtr current = store[CURRENTDOC]; + + std::vector paints; + std::vector paints_current; + std::vector paints_missing; + recurse_find_paint(document->getDefs(), paints); + + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + // Delete the server from the store if it doesn't exist in the current document + for (auto iter = current->children().begin(); iter != current->children().end();) { + Gtk::TreeRow server = *iter; + + if (std::find(paints.begin(), paints.end(), server[columns->paint]) == paints.end()) { + iter = current->erase(server); + } else { + paints_current.push_back(server[columns->paint]); + iter++; + } + } + + for (auto s : paints) { + if (std::find(paints_current.begin(), paints_current.end(), s) == paints_current.end()) { + std::cout << "missing " << s << std::endl; + paints_missing.push_back(s); + } + } + + if (!paints_missing.size()) { + return; + } + + for (auto paint : paints_missing) { + Glib::RefPtr pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + Gtk::ListStore::iterator iter = current->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = CURRENTDOC; + } +} + +void PaintServersDialog::on_target_changed() +{ + target_selected = !target_selected; +} + +void PaintServersDialog::on_document_changed() +{ + current_store = dropdown->get_active_text(); + icon_view->set_model(store[current_store]); +} + +void PaintServersDialog::on_item_activated(const Gtk::TreeModel::Path& path) +{ + // Get the current selected elements + Selection *selection = desktop->getSelection(); + std::vector const selected_items(selection->items().begin(), selection->items().end()); + + if (!selected_items.size()) { + return; + } + + PaintServersColumns *columns = getColumns(); + Gtk::ListStore::iterator iter = store[current_store]->get_iter(path); + Glib::ustring id = (*iter)[columns->id]; + Glib::ustring paint = (*iter)[columns->paint]; + Glib::RefPtr pixbuf = (*iter)[columns->pixbuf]; + Glib::ustring document_title = (*iter)[columns->document]; + SPDocument *document = document_map[document_title]; + SPObject *paint_server = document->getObjectById(id); + SPDocument *document_target = desktop->getDocument(); + + bool paint_server_exists = false; + for (auto server : store[CURRENTDOC]->children()) { + if (server[columns->id] == id) { + paint_server_exists = true; + break; + } + } + + if (!paint_server_exists) { + // Add the paint server to the current document definition + Inkscape::XML::Document *xml_doc = document_target->getReprDoc(); + Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc); + document_target->getDefs()->appendChild(repr); + Inkscape::GC::release(repr); + + // Add the pixbuf to the current document store + iter = store[CURRENTDOC]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + } + + // Recursively find elements in groups, if any + std::vector items; + for (auto item : selected_items) { + std::vector current_items = extract_elements(item); + items.insert(std::end(items), std::begin(current_items), std::end(current_items)); + } + + for (auto item : items) { + item->style->getFillOrStroke(target_selected)->read(paint.c_str()); + item->updateRepr(); + } + + document_target->collectOrphans(); +} + +std::vector PaintServersDialog::extract_elements(SPObject* item) +{ + std::vector elements; + std::vector children = item->childList(false); + if (!children.size()) { + elements.push_back(item); + } else { + for (auto e : children) { + std::vector current_items = extract_elements(e); + elements.insert(std::end(elements), std::begin(current_items), std::end(current_items)); + } + } + + return elements; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + 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 : diff --git a/src/ui/dialog/paint-servers.h b/src/ui/dialog/paint-servers.h new file mode 100644 index 0000000..d3b8f58 --- /dev/null +++ b/src/ui/dialog/paint-servers.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PAINT_SERVERS_H +#define INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +#include +#include + +#include "display/drawing.h" +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PaintServersColumns; // For Gtk::ListStore + +/** + * This dialog serves as a preview for different types of paint servers, + * currently only predefined. It can set the fill or stroke of the selected + * object to the to the paint server you select. + * + * Patterns and hatches are loaded from the preferences paths and displayed + * for each document, for all documents and for the current document. + */ + +class PaintServersDialog : public Inkscape::UI::Widget::Panel { + +public: + PaintServersDialog(gchar const *prefsPath = "/dialogs/paint"); + ~PaintServersDialog() override; + + static PaintServersDialog &getInstance() { return *new PaintServersDialog(); }; + PaintServersDialog(PaintServersDialog const &) = delete; + PaintServersDialog &operator=(PaintServersDialog const &) = delete; + + private: + static PaintServersColumns *getColumns(); + void load_sources(); + void load_document(SPDocument *document); + void load_current_document(SPObject *, guint); + Glib::RefPtr get_pixbuf(SPDocument *, Glib::ustring, Glib::ustring *); + void on_target_changed(); + void on_document_changed(); + void on_item_activated(const Gtk::TreeModel::Path &path); + std::vector extract_elements(SPObject *item); + + const Glib::ustring ALLDOCS; + const Glib::ustring CURRENTDOC; + std::map> store; + Glib::ustring current_store; + std::map document_map; + SPDocument *preview_document; + Inkscape::Drawing renderDrawing; + Gtk::ComboBoxText *dropdown; + Gtk::IconView *icon_view; + SPDesktop *desktop; + Gtk::ComboBoxText *target_dropdown; + bool target_selected; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/panel-dialog.h b/src/ui/dialog/panel-dialog.h new file mode 100644 index 0000000..eb7d9f3 --- /dev/null +++ b/src/ui/dialog/panel-dialog.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A panel holding dialog + */ +/* Authors: + * C 2007 Gustav Broberg + * C 2012 Kris De Gussem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PANEL_DIALOG_H +#define INKSCAPE_PANEL_DIALOG_H + +#include +#include + +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog/swatches.h" +#include "ui/dialog/floating-behavior.h" +#include "ui/dialog/dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Auxiliary class for the link between UI::Dialog::PanelDialog and UI::Dialog::Dialog. + * + * PanelDialog handles signals emitted when a desktop changes, either changing to a + * different desktop or a new document. + */ +class PanelDialogBase { +public: + PanelDialogBase(UI::Widget::Panel &panel, char const */*prefs_path*/, int const /*verb_num*/) : + _panel (panel) { } + + virtual void present() = 0; + virtual ~PanelDialogBase() = default; + + virtual UI::Widget::Panel &getPanel() { return _panel; } + +protected: + + inline virtual void _propagateDocumentReplaced(SPDesktop* desktop, SPDocument *document); + inline virtual void _propagateDesktopActivated(SPDesktop *); + inline virtual void _propagateDesktopDeactivated(SPDesktop *); + + UI::Widget::Panel &_panel; + sigc::connection _document_replaced_connection; +}; + +/** + * Bridges UI::Widget::Panel and UI::Dialog::Dialog. + * + * Where Dialog handles window behaviour, such as closing, position, etc, and where + * Panel is the actual container for dialog child widgets (and from where the dialog + * content is made), PanelDialog links these two classes together to create a + * dockable and floatable dialog. The link with Dialog is made via PanelDialogBase. + */ +template +class PanelDialog : public PanelDialogBase, public Inkscape::UI::Dialog::Dialog { + +public: + /** + * Constructor. + * + * @param contents panel with the actual dialog content. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + PanelDialog(UI::Widget::Panel &contents, char const *prefs_path, int const verb_num); + + ~PanelDialog() override = default; + + template + static PanelDialog *create(); + + inline void present() override; + +private: + template + static PanelDialog *_create(); + + inline void _presentDialog(); + + PanelDialog() = delete; + PanelDialog(PanelDialog const &d) = delete; // no copy + PanelDialog& operator=(PanelDialog const &d) = delete; // no assign +}; + + +void PanelDialogBase::_propagateDocumentReplaced(SPDesktop *desktop, SPDocument *document) +{ + _panel.signalDocumentReplaced().emit(desktop, document); +} + +void PanelDialogBase::_propagateDesktopActivated(SPDesktop *desktop) +{ + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialogBase::_propagateDocumentReplaced)); + _panel.signalActivateDesktop().emit(desktop); +} + +void PanelDialogBase::_propagateDesktopDeactivated(SPDesktop *desktop) +{ + _document_replaced_connection.disconnect(); + _panel.signalDeactiveDesktop().emit(desktop); +} + + +template +PanelDialog::PanelDialog(Widget::Panel &panel, char const *prefs_path, int const verb_num) : + PanelDialogBase(panel, prefs_path, verb_num), + Dialog(&B::create, prefs_path, verb_num) +{ + Gtk::Box *vbox = get_vbox(); + _panel.signalResponse().connect(sigc::mem_fun(*this, &PanelDialog::_handleResponse)); + _panel.signalPresent().connect(sigc::mem_fun(*this, &PanelDialog::_presentDialog)); + + vbox->pack_start(_panel, true, true, 0); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + _propagateDesktopActivated(desktop); + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialog::_propagateDocumentReplaced)); + + show_all_children(); +} + +template template +PanelDialog *PanelDialog::create() +{ + return _create

(); +} + +template template +PanelDialog *PanelDialog::_create() +{ + UI::Widget::Panel &panel = P::getInstance(); + return new PanelDialog(panel, panel.getPrefsPath(), panel.getVerb()); +} + +template +void PanelDialog::present() +{ + _panel.present(); +} + +template +void PanelDialog::_presentDialog() +{ + Dialog::present(); +} + +template <> inline +void PanelDialog::present() +{ + Dialog::present(); + _panel.present(); +} + +template <> inline +void PanelDialog::_presentDialog() +{ +} + +/** + * Specialized factory method for panel dialogs with floating behavior in order to make them work as + * singletons, i.e. allow them track the current active desktop. + */ +template <> +template +PanelDialog *PanelDialog::create() +{ + auto instance = _create

(); + + INKSCAPE.signal_activate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog::_propagateDesktopActivated) + ); + INKSCAPE.signal_deactivate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog::_propagateDesktopDeactivated) + ); + + return instance; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_PANEL_DIALOG_H + +/* + 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 : diff --git a/src/ui/dialog/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp new file mode 100644 index 0000000..67f1442 --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.cpp @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "verbs.h" + +#include "object/sp-ellipse.h" +#include "object/sp-item-transform.h" + +#include "ui/dialog/polar-arrange-tab.h" +#include "ui/dialog/tile.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +PolarArrangeTab::PolarArrangeTab(ArrangeDialog *parent_) + : parent(parent_), + parametersTable(), + centerY("", C_("Polar arrange tab", "Y coordinate of the center"), UNIT_TYPE_LINEAR), + centerX("", C_("Polar arrange tab", "X coordinate of the center"), centerY), + radiusY("", C_("Polar arrange tab", "Y coordinate of the radius"), UNIT_TYPE_LINEAR), + radiusX("", C_("Polar arrange tab", "X coordinate of the radius"), radiusY), + angleY("", C_("Polar arrange tab", "Ending angle"), UNIT_TYPE_RADIAL), + angleX("", C_("Polar arrange tab", "Starting angle"), angleY) +{ + anchorPointLabel.set_text(C_("Polar arrange tab", "Anchor point:")); + anchorPointLabel.set_halign(Gtk::ALIGN_START); + pack_start(anchorPointLabel, false, false); + + anchorBoundingBoxRadio.set_label(C_("Polar arrange tab", "Objects' bounding boxes:")); + anchorRadioGroup = anchorBoundingBoxRadio.get_group(); + anchorBoundingBoxRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorBoundingBoxRadio, false, false); + + pack_start(anchorSelector, false, false); + + anchorObjectPivotRadio.set_label(C_("Polar arrange tab", "Objects' rotational centers")); + anchorObjectPivotRadio.set_group(anchorRadioGroup); + anchorObjectPivotRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorObjectPivotRadio, false, false); + + arrangeOnLabel.set_text(C_("Polar arrange tab", "Arrange on:")); + arrangeOnLabel.set_halign(Gtk::ALIGN_START); + pack_start(arrangeOnLabel, false, false); + + arrangeOnFirstCircleRadio.set_label(C_("Polar arrange tab", "First selected circle/ellipse/arc")); + arrangeRadioGroup = arrangeOnFirstCircleRadio.get_group(); + arrangeOnFirstCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnFirstCircleRadio, false, false); + + arrangeOnLastCircleRadio.set_label(C_("Polar arrange tab", "Last selected circle/ellipse/arc")); + arrangeOnLastCircleRadio.set_group(arrangeRadioGroup); + arrangeOnLastCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnLastCircleRadio, false, false); + + arrangeOnParametersRadio.set_label(C_("Polar arrange tab", "Parameterized:")); + arrangeOnParametersRadio.set_group(arrangeRadioGroup); + arrangeOnParametersRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnParametersRadio, false, false); + + centerLabel.set_text(C_("Polar arrange tab", "Center X/Y:")); + parametersTable.attach(centerLabel, 0, 0, 1, 1); + centerX.setDigits(2); + centerX.setIncrements(0.2, 0); + centerX.setRange(-10000, 10000); + centerX.setValue(0, "px"); + centerY.setDigits(2); + centerY.setIncrements(0.2, 0); + centerY.setRange(-10000, 10000); + centerY.setValue(0, "px"); + parametersTable.attach(centerX, 1, 0, 1, 1); + parametersTable.attach(centerY, 2, 0, 1, 1); + + radiusLabel.set_text(C_("Polar arrange tab", "Radius X/Y:")); + parametersTable.attach(radiusLabel, 0, 1, 1, 1); + radiusX.setDigits(2); + radiusX.setIncrements(0.2, 0); + radiusX.setRange(0.001, 10000); + radiusX.setValue(100, "px"); + radiusY.setDigits(2); + radiusY.setIncrements(0.2, 0); + radiusY.setRange(0.001, 10000); + radiusY.setValue(100, "px"); + parametersTable.attach(radiusX, 1, 1, 1, 1); + parametersTable.attach(radiusY, 2, 1, 1, 1); + + angleLabel.set_text(_("Angle X/Y:")); + parametersTable.attach(angleLabel, 0, 2, 1, 1); + angleX.setDigits(2); + angleX.setIncrements(0.2, 0); + angleX.setRange(-10000, 10000); + angleX.setValue(0, "°"); + angleY.setDigits(2); + angleY.setIncrements(0.2, 0); + angleY.setRange(-10000, 10000); + angleY.setValue(180, "°"); + parametersTable.attach(angleX, 1, 2, 1, 1); + parametersTable.attach(angleY, 2, 2, 1, 1); + parametersTable.set_row_spacing(4); + parametersTable.set_column_spacing(4); + pack_start(parametersTable, false, false); + + rotateObjectsCheckBox.set_label(_("Rotate objects")); + rotateObjectsCheckBox.set_active(true); + pack_start(rotateObjectsCheckBox, false, false); + + centerX.set_sensitive(false); + centerY.set_sensitive(false); + angleX.set_sensitive(false); + angleY.set_sensitive(false); + radiusX.set_sensitive(false); + radiusY.set_sensitive(false); + + set_border_width(4); +} + +/** + * This function rotates an item around a given point by a given amount + * @param item item to rotate + * @param center center of the rotation to perform + * @param rotation amount to rotate the object by + */ +static void rotateAround(SPItem *item, Geom::Point center, Geom::Rotate const &rotation) +{ + Geom::Translate const s(center); + Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + + // Save old center + center = item->getCenter(); + + item->set_i2d_affine(item->i2dt_affine() * affine); + item->doWriteTransform(item->transform); + + if(item->isCenterSet()) + { + item->setCenter(center * affine); + item->updateRepr(); + } +} + +/** + * Calculates the angle at which to put an object given the total amount + * of objects, the index of the objects as well as the arc start and end + * points + * @param arcBegin angle at which the arc begins + * @param arcEnd angle at which the arc ends + * @param count number of objects in the selection + * @param n index of the object in the selection + */ +static float calcAngle(float arcBegin, float arcEnd, int count, int n) +{ + float arcLength = arcEnd - arcBegin; + float delta = std::abs(std::abs(arcLength) - 2*M_PI); + if(delta > 0.01) count--; // If not a complete circle, put an object also at the extremes of the arc; + + float angle = n / (float)count; + // Normalize for arcLength: + angle = angle * arcLength; + angle += arcBegin; + + return angle; +} + +/** + * Calculates the point at which an object needs to be, given the center of the ellipse, + * it's radius (x and y), as well as the angle + */ +static Geom::Point calcPoint(float cx, float cy, float rx, float ry, float angle) +{ + return Geom::Point(cx + cos(angle) * rx, cy + sin(angle) * ry); +} + +/** + * Returns the selected anchor point in desktop coordinates. If anchor + * is 0 to 8, then a bounding box point has been chosen. If it is 9 however + * the rotational center is chosen. + */ +static Geom::Point getAnchorPoint(int anchor, SPItem *item) +{ + Geom::Point source; + + Geom::OptRect bbox = item->documentVisualBounds(); + + switch(anchor) + { + case 0: // Top - Left + case 3: // Middle - Left + case 6: // Bottom - Left + source[0] = bbox->min()[Geom::X]; + break; + case 1: // Top - Middle + case 4: // Middle - Middle + case 7: // Bottom - Middle + source[0] = (bbox->min()[Geom::X] + bbox->max()[Geom::X]) / 2.0f; + break; + case 2: // Top - Right + case 5: // Middle - Right + case 8: // Bottom - Right + source[0] = bbox->max()[Geom::X]; + break; + }; + + switch(anchor) + { + case 0: // Top - Left + case 1: // Top - Middle + case 2: // Top - Right + source[1] = bbox->min()[Geom::Y]; + break; + case 3: // Middle - Left + case 4: // Middle - Middle + case 5: // Middle - Right + source[1] = (bbox->min()[Geom::Y] + bbox->max()[Geom::Y]) / 2.0f; + break; + case 6: // Bottom - Left + case 7: // Bottom - Middle + case 8: // Bottom - Right + source[1] = bbox->max()[Geom::Y]; + break; + }; + + // If using center + if(anchor == 9) + source = item->getCenter(); + else + { + source *= item->document->doc2dt(); + } + + return source; +} + +/** + * Moves an SPItem to a given location, the location is based on the given anchor point. + * @param anchor 0 to 8 are the various bounding box points like follows: + * 0 1 2 + * 3 4 5 + * 6 7 8 + * Anchor mode 9 is the rotational center of the object + * @param item Item to move + * @param p point at which to move the object + */ +static void moveToPoint(int anchor, SPItem *item, Geom::Point p) +{ + item->move_rel(Geom::Translate(p - getAnchorPoint(anchor, item))); +} + +void PolarArrangeTab::arrange() +{ + Inkscape::Selection *selection = parent->getDesktop()->getSelection(); + const std::vector tmp(selection->items().begin(), selection->items().end()); + SPGenericEllipse *referenceEllipse = nullptr; // Last ellipse in selection + + bool arrangeOnEllipse = !arrangeOnParametersRadio.get_active(); + bool arrangeOnFirstEllipse = arrangeOnEllipse && arrangeOnFirstCircleRadio.get_active(); + float yaxisdir = parent->getDesktop()->yaxisdir(); + + int count = 0; + for(auto item : tmp) + { + if(arrangeOnEllipse) + { + if(arrangeOnFirstEllipse) + { + // The first selected ellipse is actually the last one in the list + if(SP_IS_GENERICELLIPSE(item)) + referenceEllipse = SP_GENERICELLIPSE(item); + } else { + // The last selected ellipse is actually the first in list + if(SP_IS_GENERICELLIPSE(item) && referenceEllipse == nullptr) + referenceEllipse = SP_GENERICELLIPSE(item); + } + } + ++count; + } + + float cx, cy; // Center of the ellipse + float rx, ry; // Radiuses of the ellipse in x and y direction + float arcBeg, arcEnd; // begin and end angles for arcs + Geom::Affine transformation; // Any additional transformation to apply to the objects + + if(arrangeOnEllipse) + { + if(referenceEllipse == nullptr) + { + Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true); + dialog.run(); + return; + } else { + cx = referenceEllipse->cx.value; + cy = referenceEllipse->cy.value; + rx = referenceEllipse->rx.value; + ry = referenceEllipse->ry.value; + arcBeg = referenceEllipse->start; + arcEnd = referenceEllipse->end; + + transformation = referenceEllipse->i2dt_affine(); + + // We decrement the count by 1 as we are not going to lay + // out the reference ellipse + --count; + } + + } else { + // Read options from UI + cx = centerX.getValue("px"); + cy = centerY.getValue("px"); + rx = radiusX.getValue("px"); + ry = radiusY.getValue("px"); + arcBeg = angleX.getValue("rad"); + arcEnd = angleY.getValue("rad") * yaxisdir; + transformation.setIdentity(); + referenceEllipse = nullptr; + } + + int anchor = 9; + if(anchorBoundingBoxRadio.get_active()) + { + anchor = anchorSelector.getHorizontalAlignment() + + anchorSelector.getVerticalAlignment() * 3; + } + + Geom::Point realCenter = Geom::Point(cx, cy) * transformation; + + int i = 0; + for(auto item : tmp) + { + // Ignore the reference ellipse if any + if(item != referenceEllipse) + { + float angle = calcAngle(arcBeg, arcEnd, count, i); + Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation; + + moveToPoint(anchor, item, newLocation); + + if(rotateObjectsCheckBox.get_active()) { + // Calculate the angle by which to rotate each object + angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y())); + rotateAround(item, newLocation, Geom::Rotate(angle)); + } + + ++i; + } + } + + DocumentUndo::done(parent->getDesktop()->getDocument(), SP_VERB_SELECTION_ARRANGE, + _("Arrange on ellipse")); +} + +void PolarArrangeTab::updateSelection() +{ +} + +void PolarArrangeTab::on_arrange_radio_changed() +{ + bool arrangeParametric = arrangeOnParametersRadio.get_active(); + + centerX.set_sensitive(arrangeParametric); + centerY.set_sensitive(arrangeParametric); + + angleX.set_sensitive(arrangeParametric); + angleY.set_sensitive(arrangeParametric); + + radiusX.set_sensitive(arrangeParametric); + radiusY.set_sensitive(arrangeParametric); + + parametersTable.set_visible(arrangeParametric); +} + +void PolarArrangeTab::on_anchor_radio_changed() +{ + bool anchorBoundingBox = anchorBoundingBoxRadio.get_active(); + + anchorSelector.set_sensitive(anchorBoundingBox); +} + +} //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 : diff --git a/src/ui/dialog/polar-arrange-tab.h b/src/ui/dialog/polar-arrange-tab.h new file mode 100644 index 0000000..0c5acde --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/widget/anchor-selector.h" +#include "ui/dialog/arrange-tab.h" + +#include +#include +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * PolarArrangeTab is a Tab displayed in the Arrange dialog and contains + * enables the user to arrange objects on a circular or elliptical shape + */ +class PolarArrangeTab : public ArrangeTab { +public: + PolarArrangeTab(ArrangeDialog *parent_); + ~PolarArrangeTab() override = default;; + + /** + * Do the actual arrangement + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + void on_anchor_radio_changed(); + void on_arrange_radio_changed(); + +private: + PolarArrangeTab(PolarArrangeTab const &d) = delete; // no copy + void operator=(PolarArrangeTab const &d) = delete; // no assign + + ArrangeDialog *parent; + + Gtk::Label anchorPointLabel; + + Gtk::RadioButtonGroup anchorRadioGroup; + Gtk::RadioButton anchorBoundingBoxRadio; + Gtk::RadioButton anchorObjectPivotRadio; + Inkscape::UI::Widget::AnchorSelector anchorSelector; + + Gtk::Label arrangeOnLabel; + + Gtk::RadioButtonGroup arrangeRadioGroup; + Gtk::RadioButton arrangeOnFirstCircleRadio; + Gtk::RadioButton arrangeOnLastCircleRadio; + Gtk::RadioButton arrangeOnParametersRadio; + + Gtk::Grid parametersTable; + + Gtk::Label centerLabel; + Inkscape::UI::Widget::ScalarUnit centerY; + Inkscape::UI::Widget::ScalarUnit centerX; + + Gtk::Label radiusLabel; + Inkscape::UI::Widget::ScalarUnit radiusY; + Inkscape::UI::Widget::ScalarUnit radiusX; + + Gtk::Label angleLabel; + Inkscape::UI::Widget::ScalarUnit angleY; + Inkscape::UI::Widget::ScalarUnit angleX; + + Gtk::CheckButton rotateObjectsCheckBox; + + +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H */ + +/* + 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 : diff --git a/src/ui/dialog/print-colors-preview-dialog.cpp b/src/ui/dialog/print-colors-preview-dialog.cpp new file mode 100644 index 0000000..6371af9 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print Colors Preview dialog - implementation. + */ +/* Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* +#include "desktop.h" +#include "print-colors-preview-dialog.h" +#include "preferences.h" +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +//Yes, I know we shouldn't hardcode CMYK. This class needs to be refactored +// in order to accommodate spot colors and color components defined using +// ICC colors. --Juca + +void PrintColorsPreviewDialog::toggle_cyan(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/cyan", cyan->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_magenta(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/magenta", magenta->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_yellow(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/yellow", yellow->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_black(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/black", black->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +PrintColorsPreviewDialog::PrintColorsPreviewDialog() + : UI::Widget::Panel("/dialogs/printcolorspreview", SP_VERB_DIALOG_PRINT_COLORS_PREVIEW) +{ + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + + cyan = new Gtk::ToggleButton(_("Cyan")); + vbox->pack_start( *cyan, false, false ); +// tips.set_tip((*cyan), _("Render cyan separation")); + cyan->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_cyan) ); + + magenta = new Gtk::ToggleButton(_("Magenta")); + vbox->pack_start( *magenta, false, false ); +// tips.set_tip((*magenta), _("Render magenta separation")); + magenta->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_magenta) ); + + yellow = new Gtk::ToggleButton(_("Yellow")); + vbox->pack_start( *yellow, false, false ); +// tips.set_tip((*yellow), _("Render yellow separation")); + yellow->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_yellow) ); + + black = new Gtk::ToggleButton(_("Black")); + vbox->pack_start( *black, false, false ); +// tips.set_tip((*black), _("Render black separation")); + black->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_black) ); + + gint val; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + val = prefs->getBool("/options/printcolorspreview/cyan"); + cyan->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/magenta"); + magenta->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/yellow"); + yellow->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/black"); + black->set_active( val != 0 ); + + _getContents()->add(*vbox); + _getContents()->show_all(); +} + +PrintColorsPreviewDialog::~PrintColorsPreviewDialog(){} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +*/ diff --git a/src/ui/dialog/print-colors-preview-dialog.h b/src/ui/dialog/print-colors-preview-dialog.h new file mode 100644 index 0000000..e40f987 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print Colors Preview dialog + */ +/* Authors: + * Felipe Corrêa da Silva Sanches + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H +#define INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H + +#include "ui/widget/panel.h" +#include "verbs.h" + +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PrintColorsPreviewDialog : public UI::Widget::Panel { +public: + PrintColorsPreviewDialog(); + ~PrintColorsPreviewDialog(); + + static PrintColorsPreviewDialog &getInstance() + { return *new PrintColorsPreviewDialog(); } + +private: + void toggle_cyan(); + void toggle_magenta(); + void toggle_yellow(); + void toggle_black(); + + Gtk::ToggleButton* cyan; + Gtk::ToggleButton* magenta; + Gtk::ToggleButton* yellow; + Gtk::ToggleButton* black; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //#ifndef INKSCAPE_UI_PRINT_COLORS_PREVIEW_H diff --git a/src/ui/dialog/print.cpp b/src/ui/dialog/print.cpp new file mode 100644 index 0000000..006961b --- /dev/null +++ b/src/ui/dialog/print.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print dialog. + */ +/* Authors: + * Kees Cook + * Abhishek Sharma + * Patrick McDermott + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2017 Patrick McDermott + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include + +#include "inkscape.h" +#include "preferences.h" +#include "print.h" + +#include "extension/internal/cairo-render-context.h" +#include "extension/internal/cairo-renderer.h" +#include "document.h" + +#include "util/units.h" +#include "helper/png-write.h" +#include "svg/svg-color.h" + +#include + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Print::Print(SPDocument *doc, SPItem *base) : + _doc (doc), + _base (base) +{ + g_assert (_doc); + g_assert (_base); + + _printop = Gtk::PrintOperation::create(); + + // set up dialog title, based on document name + const Glib::ustring jobname = _doc->getDocumentName() ? _doc->getDocumentName() : _("SVG Document"); + Glib::ustring title = _("Print"); + title += " "; + title += jobname; + _printop->set_job_name(title); + + _printop->set_unit(Gtk::UNIT_POINTS); + Glib::RefPtr page_setup = Gtk::PageSetup::create(); + + // Default to a custom paper size, in case we can't find a more specific size + gdouble doc_width = _doc->getWidth().value("pt"); + gdouble doc_height = _doc->getHeight().value("pt"); + page_setup->set_paper_size( + Gtk::PaperSize("custom", "custom", doc_width, doc_height, Gtk::UNIT_POINTS)); + + // Some print drivers, like the EPSON's ESC/P-R CUPS driver, don't accept custom + // page sizes, so we'll try to find a known page size. + // GTK+'s known paper sizes always have a longer height than width, so we'll rotate + // the page and set its orientation to landscape as necessary in order to match a paper size. + // Unfortunately, some printers, like Epilog laser cutters, don't understand landscape + // mode. + // As a compromise, we'll only rotate the page if we actually find a matching paper size, + // since laser cutter beds tend to be custom sizes. + Gtk::PageOrientation orientation = Gtk::PAGE_ORIENTATION_PORTRAIT; + if (_doc->getWidth().value("pt") > _doc->getHeight().value("pt")) { + orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE; + std::swap(doc_width, doc_height); + } + + // attempt to match document size against known paper sizes + std::vector known_sizes = Gtk::PaperSize::get_paper_sizes(false); + for (auto& size : known_sizes) { + if (fabs(size.get_width(Gtk::UNIT_POINTS) - doc_width) >= 1.0) { + // width (short edge) doesn't match + continue; + } + if (fabs(size.get_height(Gtk::UNIT_POINTS) - doc_height) >= 1.0) { + // height (short edge) doesn't match + continue; + } + // size matches + page_setup->set_paper_size(size); + page_setup->set_orientation(orientation); + break; + } + + _printop->set_default_page_setup(page_setup); + _printop->set_use_full_page(true); + + // set up signals + _workaround._doc = _doc; + _workaround._base = _base; + _workaround._tab = &_tab; + _printop->signal_create_custom_widget().connect(sigc::mem_fun(*this, &Print::create_custom_widget)); + _printop->signal_begin_print().connect(sigc::mem_fun(*this, &Print::begin_print)); + _printop->signal_draw_page().connect(sigc::mem_fun(*this, &Print::draw_page)); + + // build custom preferences tab + _printop->set_custom_tab_label(_("Rendering")); +} + +void Print::draw_page(const Glib::RefPtr& context, int /*page_nr*/) +{ + // TODO: If the user prints multiple copies we render the whole page for each copy + // It would be more efficient to render the page once (e.g. in "begin_print") + // and simply print this result as often as necessary + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + //printf("%s %d\n",__FUNCTION__, page_nr); + + if (_workaround._tab->as_bitmap()) { + // Render as exported PNG + prefs->setBool("/dialogs/printing/asbitmap", true); + gdouble width = (_workaround._doc)->getWidth().value("px"); + gdouble height = (_workaround._doc)->getHeight().value("px"); + gdouble dpi = _workaround._tab->bitmap_dpi(); + prefs->setDouble("/dialogs/printing/dpi", dpi); + + std::string tmp_png; + std::string tmp_base = "inkscape-print-png-XXXXXX"; + + int tmp_fd; + if ( (tmp_fd = Glib::file_open_tmp(tmp_png, tmp_base)) >= 0) { + close(tmp_fd); + + guint32 bgcolor = 0x00000000; + Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")){ + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")){ + double opacity = 1.0; + sp_repr_get_double (nv, "inkscape:pageopacity", &opacity); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + + sp_export_png_file(_workaround._doc, tmp_png.c_str(), 0.0, 0.0, + width, height, + (unsigned long)(Inkscape::Util::Quantity::convert(width, "px", "in") * dpi), + (unsigned long)(Inkscape::Util::Quantity::convert(height, "px", "in") * dpi), + dpi, dpi, bgcolor, nullptr, nullptr, true, std::vector()); + + // This doesn't seem to work: + //context->set_cairo_context ( Cairo::Context::create (Cairo::ImageSurface::create_from_png (tmp_png) ), dpi, dpi ); + // + // so we'll use a surface pattern blat instead... + // + // but the C++ interface isn't implemented in cairomm: + //context->get_cairo_context ()->set_source_surface(Cairo::ImageSurface::create_from_png (tmp_png) ); + // + // so do it in C: + { + Cairo::RefPtr png = Cairo::ImageSurface::create_from_png (tmp_png); + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_matrix_t m; + cairo_get_matrix(cr, &m); + cairo_scale(cr, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi); + // FIXME: why is the origin offset?? + cairo_set_source_surface(cr, png->cobj(), 0, 0); + cairo_paint(cr); + cairo_set_matrix(cr, &m); + } + + // Clean up + unlink (tmp_png.c_str()); + } + else { + g_warning("%s", _("Could not open temporary PNG for bitmap printing")); + } + } + else { + // Render as vectors + prefs->setBool("/dialogs/printing/asbitmap", false); + Inkscape::Extension::Internal::CairoRenderer renderer; + Inkscape::Extension::Internal::CairoRenderContext *ctx = renderer.createContext(); + + // ctx->setPSLevel(CAIRO_PS_LEVEL_3); + ctx->setTextToPath(false); + ctx->setFilterToBitmap(true); + ctx->setBitmapResolution(72); + + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_surface_t *surface = cairo_get_target(cr); + cairo_matrix_t ctm; + cairo_get_matrix(cr, &ctm); + + bool ret = ctx->setSurfaceTarget (surface, true, &ctm); + if (ret) { + ret = renderer.setupDocument (ctx, _workaround._doc, TRUE, 0., nullptr); + if (ret) { + renderer.renderItem(ctx, _workaround._base); + ctx->finish(false); // do not finish the cairo_surface_t - it's owned by our GtkPrintContext! + } + else { + g_warning("%s", _("Could not set up Document")); + } + } + else { + g_warning("%s", _("Failed to set CairoRenderContext")); + } + + // Clean up + renderer.destroyContext(ctx); + } + +} + +Gtk::Widget *Print::create_custom_widget() +{ + //printf("%s\n",__FUNCTION__); + return &_tab; +} + +void Print::begin_print(const Glib::RefPtr&) +{ + //printf("%s\n",__FUNCTION__); + _printop->set_n_pages(1); +} + +Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window) +{ + // Remember to restore the previous print settings + _printop->set_print_settings(SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings); + + try { + Gtk::PrintOperationResult res = _printop->run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parent_window); + + // Save printer settings (but only on success) + if (res == Gtk::PRINT_OPERATION_RESULT_APPLY) { + SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings = _printop->get_print_settings(); + } + + return res; + } catch (const Glib::Error &e) { + g_warning("Failed to print '%s': %s", _doc->getDocumentName(), e.what().c_str()); + } + + return Gtk::PRINT_OPERATION_RESULT_ERROR; +} + + +} // 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 : diff --git a/src/ui/dialog/print.h b/src/ui/dialog/print.h new file mode 100644 index 0000000..d015210 --- /dev/null +++ b/src/ui/dialog/print.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print dialog + */ +/* Authors: + * Kees Cook + * + * Copyright (C) 2007 Kees Cook + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_H +#define INKSCAPE_UI_DIALOG_PRINT_H + +#include "ui/widget/rendering-options.h" +#include // GtkMM + +class SPItem; +class SPDocument; + + +/* + * gtk 2.12.0 has a bug (http://bugzilla.gnome.org/show_bug.cgi?id=482089) + * where it fails to correctly deal with gtkmm signal management. As a result + * we have call gtk directly instead of doing a much cleaner version of + * this printing dialog, using full gtkmmification. (The bug was fixed + * in 2.12.1, so when the Inkscape gtk minimum version is bumped there, + * we can revert Inkscape commit 16865. + */ +struct workaround_gtkmm +{ + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions *_tab; +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct PrinterSettings { + Glib::RefPtr _gtk_print_settings; +}; + +class Print { +public: + Print(SPDocument *doc, SPItem *base); + Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window); + +protected: + +private: + Glib::RefPtr _printop; + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions _tab; + + struct workaround_gtkmm _workaround; + + void draw_page(const Glib::RefPtr& context, int /*page_nr*/); + Gtk::Widget *create_custom_widget(); + void begin_print(const Glib::RefPtr&); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_PRINT_H + +/* + 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 : diff --git a/src/ui/dialog/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp new file mode 100644 index 0000000..5a8afa9 --- /dev/null +++ b/src/ui/dialog/save-template-dialog.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "save-template-dialog.h" +#include "file.h" +#include "io/resource.h" + +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +SaveTemplate::SaveTemplate() : + Gtk::Dialog(_("Save Document as Template")), + name_label(_("Name: "), Gtk::ALIGN_START), + author_label(_("Author: "), Gtk::ALIGN_START), + description_label(_("Description: "), Gtk::ALIGN_START), + keywords_label(_("Keywords: "), Gtk::ALIGN_START), + is_default_template(_("Set as default template")) +{ + resize(400, 200); + + name_text.set_hexpand(true); + + grid.attach(name_label, 0, 0, 1, 1); + grid.attach(name_text, 1, 0, 1, 1); + + grid.attach(author_label, 0, 1, 1, 1); + grid.attach(author_text, 1, 1, 1, 1); + + grid.attach(description_label, 0, 2, 1, 1); + grid.attach(description_text, 1, 2, 1, 1); + + grid.attach(keywords_label, 0, 3, 1, 1); + grid.attach(keywords_text, 1, 3, 1, 1); + + auto content_area = get_content_area(); + + content_area->set_spacing(5); + + content_area->add(grid); + content_area->add(is_default_template); + + name_text.signal_changed().connect( sigc::mem_fun(*this, + &SaveTemplate::on_name_changed) ); + + add_button("Cancel", Gtk::RESPONSE_CANCEL); + add_button("Save", Gtk::RESPONSE_OK); + + set_response_sensitive(Gtk::RESPONSE_OK, false); + set_default_response(Gtk::RESPONSE_CANCEL); + + show_all(); +} + +void SaveTemplate::on_name_changed() { + + if (name_text.get_text_length() == 0) { + + set_response_sensitive(Gtk::RESPONSE_OK, false); + } else { + + set_response_sensitive(Gtk::RESPONSE_OK, true); + } +} + +bool SaveTemplate::save_template(Gtk::Window &parentWindow) { + + return sp_file_save_template(parentWindow, name_text.get_text(), + author_text.get_text(), description_text.get_text(), + keywords_text.get_text(), is_default_template.get_active()); +} + +void SaveTemplate::save_document_as_template(Gtk::Window &parentWindow) { + + SaveTemplate dialog; + + auto operation_done = false; + + while (operation_done == false) { + + auto user_response = dialog.run(); + + if (user_response == Gtk::RESPONSE_OK) + operation_done = dialog.save_template(parentWindow); + else + operation_done = true; + } +} + +} +} +} diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h new file mode 100644 index 0000000..8cf2d8e --- /dev/null +++ b/src/ui/dialog/save-template-dialog.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H + +#include +#include +#include +#include +#include +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SaveTemplate : public Gtk::Dialog +{ + +public: + + static void save_document_as_template(Gtk::Window &parentWindow); + +protected: + + void on_name_changed(); + +private: + + Gtk::Grid grid; + + Gtk::Label name_label; + Gtk::Entry name_text; + + Gtk::Label author_label; + Gtk::Entry author_text; + + Gtk::Label description_label; + Gtk::Entry description_text; + + Gtk::Label keywords_label; + Gtk::Entry keywords_text; + + Gtk::CheckButton is_default_template; + + SaveTemplate(); + bool save_template(Gtk::Window &parentWindow); + +}; +} +} +} +#endif diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp new file mode 100644 index 0000000..280bfdf --- /dev/null +++ b/src/ui/dialog/selectorsdialog.cpp @@ -0,0 +1,1508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * Jabiertxof + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 + * Copyright (C) Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selectorsdialog.h" +#include "attribute-rel-svg.h" +#include "document-undo.h" +#include "inkscape.h" +#include "selection.h" +#include "style.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/iconrenderer.h" +#include "verbs.h" + +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +#include +#include + +#include +#include +#include + +// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape +// #define DEBUG_SELECTORSDIALOG +// #define G_LOG_DOMAIN "SELECTORSDIALOG" + +using Inkscape::DocumentUndo; +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; + +/** + * This macro is used to remove spaces around selectors or any strings when + * parsing is done to update XML style element or row labels in this dialog. + */ +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + if (x.size() && x[0] == ',') \ + x.erase(0, 1); \ + if (x.size() && x[x.size() - 1] == ',') \ + x.erase(x.size() - 1, 1); \ + x.erase(x.find_last_not_of(' ') + 1); + +namespace Inkscape { +namespace UI { +namespace Dialog { + +// Keeps a watch on style element +class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver { + public: + NodeObserver(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeObserver: Constructor"); + }; + + void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + + SelectorsDialog *_selectorsdialog; +}; + + +void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + + g_debug("SelectorsDialog::NodeObserver::notifyContentChanged"); + _selectorsdialog->_scroollock = true; + _selectorsdialog->_updating = false; + _selectorsdialog->_readStyleElement(); + _selectorsdialog->_selectRow(); +} + + +// Keeps a watch for new/removed/changed nodes +// (Must update objects that selectors match.) +class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver { + public: + NodeWatcher(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeWatcher: Constructor"); + }; + + void notifyChildAdded( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeAdded(child); + } + + void notifyChildRemoved( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeRemoved(child); + } + + void notifyAttributeChanged( Inkscape::XML::Node &node, + GQuark qname, + Util::ptr_shared /*old_value*/, + Util::ptr_shared /*new_value*/ ) override { + + static GQuark const CODE_id = g_quark_from_static_string("id"); + static GQuark const CODE_class = g_quark_from_static_string("class"); + + if (qname == CODE_id || qname == CODE_class) { + _selectorsdialog->_nodeChanged(node); + } + } + + SelectorsDialog *_selectorsdialog; +}; + +void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node) +{ + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (_textNode == &repr) { + _textNode = nullptr; + } + + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + + g_debug("SelectorsDialog::NodeChanged"); + + _scroollock = true; + + _readStyleElement(); + _selectRow(); +} + +SelectorsDialog::TreeStore::TreeStore() = default; + + +/** + * Allow dragging only selectors. + */ +bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const +{ + g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc"); + + auto unconstThis = const_cast(this); + const_iterator iter = unconstThis->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR; + return is_draggable; + } + return Gtk::TreeStore::row_draggable_vfunc(path); +} + +/** + * Allow dropping only in between other selectors. + */ +bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest, + const Gtk::SelectionData &selection_data) const +{ + g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc"); + + Gtk::TreeModel::Path dest_parent = dest; + dest_parent.up(); + return dest_parent.empty(); +} + + +// This is only here to handle updating style element after a drag and drop. +void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path) +{ + if (_selectorsdialog->_updating) + return; // Don't write if we deleted row (other than from DND) + + g_debug("on_row_deleted"); + _selectorsdialog->_writeStyleElement(); + _selectorsdialog->_readStyleElement(); +} + + +Glib::RefPtr SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog) +{ + g_debug("SelectorsDialog::TreeStore::create"); + + SelectorsDialog::TreeStore *store = new SelectorsDialog::TreeStore(); + store->_selectorsdialog = selectorsdialog; + store->set_column_types(store->_selectorsdialog->_mColumns); + return Glib::RefPtr(store); +} + +/** + * Constructor + * A treeview and a set of two buttons are added to the dialog. _addSelector + * adds selectors to treeview. _delSelector deletes the selector from the dialog. + * Any addition/deletion of the selectors updates XML style element accordingly. + */ +SelectorsDialog::SelectorsDialog() + : UI::Widget::Panel("/dialogs/selectors", SP_VERB_DIALOG_SELECTORS) + , _updating(false) + , _textNode(nullptr) + , _scroolpos(0) + , _scroollock(false) + , _desktopTracker() +{ + g_debug("SelectorsDialog::SelectorsDialog"); + + m_nodewatcher.reset(new SelectorsDialog::NodeWatcher(this)); + m_styletextwatcher.reset(new SelectorsDialog::NodeObserver(this)); + + // Tree + Inkscape::UI::Widget::IconRenderer * addRenderer = manage( + new Inkscape::UI::Widget::IconRenderer() ); + addRenderer->add_icon("edit-delete"); + addRenderer->add_icon("list-add"); + addRenderer->add_icon("empty-icon"); + _store = TreeStore::create(this); + _treeView.set_model(_store); + + _treeView.set_headers_visible(false); + _treeView.enable_model_drag_source(); + _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE ); + int addCol = _treeView.append_column("", *addRenderer) - 1; + Gtk::TreeViewColumn *col = _treeView.get_column(addCol); + if ( col ) { + col->add_attribute(addRenderer->property_icon(), _mColumns._colType); + } + + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + addCol = _treeView.append_column("CSS Selector", *label) - 1; + col = _treeView.get_column(addCol); + if (col) { + col->add_attribute(label->property_text(), _mColumns._colSelector); + col->add_attribute(label->property_weight(), _mColumns._colSelected); + } + _treeView.set_expander_column(*(_treeView.get_column(1))); + + + // Signal handlers + _treeView.signal_button_release_event().connect( // Needs to be release, not press. + sigc::mem_fun(*this, &SelectorsDialog::_handleButtonEvent), false); + + _treeView.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &SelectorsDialog::_buttonEventsSelectObjs), false); + + _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand)); + + _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse)); + + _showWidgets(); + + // Document & Desktop + _desktop_changed_connection = + _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &SelectorsDialog::_handleDesktopChanged)); + _desktopTracker.connect(GTK_WIDGET(gobj())); + + _document_replaced_connection = + getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _selection_changed_connection = getDesktop()->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + // Add watchers + _updateWatchers(getDesktop()); + + // Load tree + _readStyleElement(); + _selectRow(); + + if (!_store->children().empty()) { + _del.show(); + } + show_all(); +} + + +void SelectorsDialog::_vscrool() +{ + if (!_scroollock) { + _scroolpos = _vadj->get_value(); + } else { + _vadj->set_value(_scroolpos); + _scroollock = false; + } +} + +void SelectorsDialog::_showWidgets() +{ + // Pack widgets + g_debug("SelectorsDialog::_showWidgets"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = prefs->getBool("/dialogs/selectors/vertical", true); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _selectors_box.set_orientation(Gtk::ORIENTATION_VERTICAL); + _selectors_box.set_name("SelectorsDialog"); + _scrolled_window_selectors.add(_treeView); + _scrolled_window_selectors.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _vadj = _scrolled_window_selectors.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscrool)); + _selectors_box.pack_start(_scrolled_window_selectors, Gtk::PACK_EXPAND_WIDGET); + /* Gtk::Label *dirtogglerlabel = Gtk::manage(new Gtk::Label(_("Paned vertical"))); + dirtogglerlabel->get_style_context()->add_class("inksmall"); + _direction.property_active() = dir; + _direction.property_active().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection)); + _direction.get_style_context()->add_class("inkswitch"); */ + _styleButton(_create, "list-add", "Add a new CSS Selector"); + _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector)); + _styleButton(_del, "list-remove", "Remove a CSS Selector"); + _button_box.pack_start(_create, Gtk::PACK_SHRINK); + _button_box.pack_start(_del, Gtk::PACK_SHRINK); + Gtk::RadioButton::Group group; + Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton()); + Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton()); + _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal")); + _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical")); + _horizontal->set_group(group); + _vertical->set_group(group); + _vertical->set_active(dir); + _vertical->signal_toggled().connect( + sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical)); + _horizontal->property_draw_indicator() = false; + _vertical->property_draw_indicator() = false; + _button_box.pack_end(*_horizontal, false, false, 0); + _button_box.pack_end(*_vertical, false, false, 0); + _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector)); + _del.hide(); + _style_dialog = new StyleDialog; + _style_dialog->set_name("StyleDialog"); + _paned.pack1(*_style_dialog, Gtk::SHRINK); + _paned.pack2(_selectors_box, true, true); + _paned.set_wide_handle(true); + Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET); + contents->pack_start(_button_box, false, false, 0); + contents->set_valign(Gtk::ALIGN_FILL); + contents->child_property_fill(_paned); + Gtk::ScrolledWindow *dialog_scroller = new Gtk::ScrolledWindow(); + dialog_scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + dialog_scroller->set_shadow_type(Gtk::SHADOW_IN); + dialog_scroller->add(*Gtk::manage(contents)); + _getContents()->pack_start(*dialog_scroller, Gtk::PACK_EXPAND_WIDGET); + show_all(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + int panedpos = prefs->getInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_childresized)); + _paned.signal_size_allocate().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedresized)); + _paned.signal_realize().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedrealized)); + _updating = true; + _paned.property_position() = panedpos; + _updating = false; + set_size_request(320, 260); + set_name("SelectorsAndStyleDialog"); +} + +void SelectorsDialog::_panedresized(Gtk::Allocation allocation) +{ + g_debug("SelectorsDialog::_panedresized"); + _resized(); +} + +void SelectorsDialog::_panedrealized() { _style_dialog->readStyleElement(); } + +void SelectorsDialog::_childresized() +{ + g_debug("SelectorsDialog::_childresized"); + _resized(); +} + +void SelectorsDialog::_resized() +{ + g_debug("SelectorsDialog::_resized"); + _scroollock = true; + if (_updating) { + return; + } + _updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = !prefs->getBool("/dialogs/selectors/vertical", true); + int max = int(_paned.property_max_position() * 0.95); + int min = int(_paned.property_max_position() * 0.05); + if (_paned.property_position() > max) { + _paned.property_position() = max; + } + if (_paned.property_position() < min) { + _paned.property_position() = min; + } + + prefs->setInt("/dialogs/selectors/panedpos", _paned.property_position()); + _updating = false; +} + +void SelectorsDialog::_toggleDirection(Gtk::RadioButton *vertical) +{ + g_debug("SelectorsDialog::_toggleDirection"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = vertical->get_active(); + prefs->setBool("/dialogs/selectors/vertical", dir); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position() = widthpos / 2; +} + +/** + * Class destructor + */ +SelectorsDialog::~SelectorsDialog() +{ + g_debug("SelectorsDialog::~SelectorsDialog"); + _desktop_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + _selection_changed_connection.disconnect(); +} + + +/** + * @return Inkscape::XML::Node* pointing to a style element's text node. + * Returns the style element's text node. If there is no style element, one is created. + * Ditto for text node. + */ +Inkscape::XML::Node *SelectorsDialog::_getStyleTextNode(bool create_if_missing) +{ + g_debug("SelectorsDialog::_getStyleTextNode"); + + auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing); + + if (_textNode != textNode) { + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + } + + _textNode = textNode; + + if (_textNode) { + _textNode->addObserver(*m_styletextwatcher); + } + } + + return textNode; +} + +/** + * Fill the Gtk::TreeStore from the svg:style element. + */ +void SelectorsDialog::_readStyleElement() +{ + g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false")); + + if (_updating) return; // Don't read if we wrote style element. + _updating = true; + _scroollock = true; + Inkscape::XML::Node * textNode = _getStyleTextNode(); + + // Get content from style text node. + std::string content = (textNode && textNode->content()) ? textNode->content() : ""; + + // Remove end-of-lines (check it works on Windoze). + content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); + + // Remove comments (/* xxx */) +#if 0 + while(content.find("/*") != std::string::npos) { + size_t start = content.find("/*"); + content.erase(start, (content.find("*\/", start) - start) +2); + } +#endif + + // First split into selector/value chunks. + // An attempt to use Glib::Regex failed. A C++11 version worked but + // reportedly has problems on Windows. Using split_simple() is simpler + // and probably faster. + // + // Glib::RefPtr regex1 = + // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); + // + // Glib::MatchInfo minfo; + // regex1->match(content, minfo); + + // Split on curly brackets. Even tokens are selectors, odd are values. + std::vector tokens = Glib::Regex::split_simple("[}{]", content); + + // If text node is empty, return (avoids problem with negative below). + if (tokens.size() == 0) { + _store->clear(); + _updating = false; + return; + } + _treeView.show_all(); + std::vector> expanderstatus; + for (unsigned i = 0; i < tokens.size() - 1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + std::vector selectordata = Glib::Regex::split_simple(";", selector); + if (!selectordata.empty()) { + selector = selectordata.back(); + } + selector = _style_dialog->fixCSSSelectors(selector); + for (auto &row : _store->children()) { + Glib::ustring selectorold = row[_mColumns._colSelector]; + if (selectorold == selector) { + expanderstatus.emplace_back(selector, row[_mColumns._colExpand]); + } + } + } + _store->clear(); + bool rewrite = false; + + + std::vector objVec; + for (unsigned i = 0; i < tokens.size()-1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + std::vector selectordata = Glib::Regex::split_simple(";", selector); + for (auto selectoritem : selectordata) { + if (selectordata[selectordata.size() - 1] == selectoritem) { + selector = selectoritem; + } else { + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selectoritem + ";"; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } + } + Glib::ustring selector_old = selector; + selector = _style_dialog->fixCSSSelectors(selector); + if (selector_old != selector) { + rewrite = true; + } + + if (selector.empty() || selector == "* > .inkscapehacktmp") { + continue; + } + std::vector tokensplus = Glib::Regex::split_simple("[,]+", selector); + coltype colType = SELECTOR; + // Get list of objects selector matches + objVec = _getObjVec(selector); + + Glib::ustring properties; + // Check to make sure we do have a value to match selector. + if ((i+1) < tokens.size()) { + properties = tokens[i+1]; + } else { + std::cerr << "SelectorsDialog::_readStyleElement(): Missing values " + "for last selector!" + << std::endl; + } + REMOVE_SPACES(properties); + bool colExpand = false; + for (auto rowstatus : expanderstatus) { + if (selector == rowstatus.first) { + colExpand = rowstatus.second; + } + } + std::vector properties_data = Glib::Regex::split_simple(";", properties); + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selector; + row[_mColumns._colExpand] = colExpand; + row[_mColumns._colType] = colType; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = properties; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + // Add as children, objects that match selector. + for (auto &obj : objVec) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->append(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = colType == OBJECT; + childrow[_mColumns._colObj] = std::vector(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + + + _updating = false; + if (rewrite) { + _writeStyleElement(); + } + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_expand()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = true; +} + +void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_collapse()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = false; +} +/** + * Update the content of the style element as selectors (or objects) are added/removed. + */ +void SelectorsDialog::_writeStyleElement() +{ + + if (_updating) { + return; + } + + g_debug("SelectorsDialog::_writeStyleElement"); + + _scroollock = true; + _updating = true; + Glib::ustring styleContent = ""; + for (auto& row: _store->children()) { + Glib::ustring selector = row[_mColumns._colSelector]; +#if 0 + REMOVE_SPACES(selector); + size_t len = selector.size(); + if(selector[len-1] == ','){ + selector.erase(len-1); + } + row[_mColumns._colSelector] = selector; +#endif + if (row[_mColumns._colType] == OTHER) { + styleContent = selector + styleContent; + } else { + styleContent = styleContent + selector + " { " + row[_mColumns._colProperties] + " }\n"; + } + } + // We could test if styleContent is empty and then delete the style node here but there is no + // harm in keeping it around ... + Inkscape::XML::Node *textNode = _getStyleTextNode(true); + bool empty = false; + if (styleContent.empty()) { + empty = true; + styleContent = "* > .inkscapehacktmp{}"; + } + textNode->setContent(styleContent.c_str()); + INKSCAPE.readStyleSheets(true); + if (empty) { + styleContent = ""; + textNode->setContent(styleContent.c_str()); + } + textNode->setContent(styleContent.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_SELECTORS, _("Edited style element.")); + + _updating = false; + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +/** + * Update the watchers on objects. + */ +void SelectorsDialog::_updateWatchers(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::_updateWatchers"); + + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } + + if (desktop) { + m_root = desktop->getDocument()->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } +} +/* +void sp_get_selector_active(Glib::ustring &selector) +{ + std::vector tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + REMOVE_SPACES(selector); + Glib::ustring toadd = Glib::ustring(selector); + Glib::ustring toparse = Glib::ustring(selector); + Glib::ustring tag = ""; + if (toadd[0] != '.' || toadd[0] != '#') { + auto i = std::min(toadd.find("#"), toadd.find(".")); + tag = toadd.substr(0,i-1); + toparse.erase(0, i-1); + } + auto i = toparse.find("#"); + toparse.erase(i, 1); + auto j = toparse.find("#"); + if (j == std::string::npos) { + selector = ""; + } else if (i != std::string::npos) { + Glib::ustring post = toadd.substr(0,i-1); + Glib::ustring pre = toadd.substr(i, (toadd.size()-1)-i); + selector = tag + pre + post; + } +} */ + +Glib::ustring sp_get_selector_classes(Glib::ustring selector) //, SelectorType selectortype, Glib::ustring id = "") +{ + g_debug("SelectorsDialog::sp_get_selector_classes"); + + std::pair result; + std::vector tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + REMOVE_SPACES(selector); + Glib::ustring toparse = Glib::ustring(selector); + selector = Glib::ustring(""); + auto i = toparse.find("."); + if (i == std::string::npos) { + return ""; + } + if (toparse[0] != '.' && toparse[0] != '#') { + i = std::min(toparse.find("#"), toparse.find(".")); + Glib::ustring tag = toparse.substr(0, i); + if (!SPAttributeRelSVG::isSVGElement(tag)) { + return selector; + } + if (i != std::string::npos) { + toparse.erase(0, i); + } + } + i = toparse.find("#"); + if (i != std::string::npos) { + toparse.erase(i, 1); + } + auto j = toparse.find("#"); + if (j != std::string::npos) { + return selector; + } + if (i != std::string::npos) { + toparse.insert(i, "#"); + if (i) { + Glib::ustring post = toparse.substr(0, i); + Glib::ustring pre = toparse.substr(i, toparse.size() - i); + toparse = pre + post; + } + auto k = toparse.find("."); + if (k != std::string::npos) { + toparse = toparse.substr(k, toparse.size() - k); + } + } + return toparse; +} + +/** + * @param row + * Add selected objects on the desktop to the selector corresponding to 'row'. + */ +void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_addToSelector: Entrance"); + if (*row) { + // Store list of selected elements on desktop (not to be confused with selector). + _updating = true; + if (row[_mColumns._colType] == OTHER) { + return; + } + Inkscape::Selection *selection = getDesktop()->getSelection(); + std::vector toAddObjVec(selection->objects().begin(), selection->objects().end()); + Glib::ustring multiselector = row[_mColumns._colSelector]; + std::vector objVec = _getObjVec(multiselector); + row[_mColumns._colObj] = objVec; + row[_mColumns._colExpand] = true; + std::vector tokens = Glib::Regex::split_simple("[,]+", multiselector); + for (auto &obj : toAddObjVec) { + auto *id = obj->getId(); + if (!id) + continue; + for (auto tok : tokens) { + Glib::ustring clases = sp_get_selector_classes(tok); + if (!clases.empty()) { + _insertClass(obj, clases); + std::vector currentobjs = _getObjVec(multiselector); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + std::vector currentobjs = _getObjVec(multiselector); + bool insertid = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + insertid = false; + } + } + if (insertid) { + multiselector = multiselector + ",#" + id; + } + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = std::vector(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + objVec = _getObjVec(multiselector); + row[_mColumns._colSelector] = multiselector; + row[_mColumns._colObj] = objVec; + _updating = false; + + // Add entry to style element + for (auto &obj : toAddObjVec) { + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + SPCSSAttr *css_selector = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + Glib::ustring selprops = row[_mColumns._colProperties]; + sp_repr_css_attr_add_from_string(css_selector, selprops.c_str()); + for (List iter = css_selector->attributeList(); iter; ++iter) { + gchar const *key = g_quark_to_string(iter->key); + css->setAttribute(key, nullptr); + } + sp_repr_css_write_string(css, css_str); + sp_repr_css_attr_unref(css); + sp_repr_css_attr_unref(css_selector); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + _writeStyleElement(); + } +} + +/** + * @param row + * Remove the object corresponding to 'row' from the parent selector. + */ +void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_removeFromSelector: Entrance"); + if (*row) { + _scroollock = true; + _updating = true; + SPObject *obj = nullptr; + Glib::ustring objectLabel = row[_mColumns._colSelector]; + Gtk::TreeModel::iterator iter = row->parent(); + if (iter) { + Gtk::TreeModel::Row parent = *iter; + Glib::ustring multiselector = parent[_mColumns._colSelector]; + REMOVE_SPACES(multiselector); + obj = _getObjVec(objectLabel)[0]; + std::vector tokens = Glib::Regex::split_simple("[,]+", multiselector); + Glib::ustring selector = ""; + for (auto tok : tokens) { + if (tok.empty()) { + continue; + } + // TODO: handle when other selectors has the removed class applied to maybe not remove + Glib::ustring clases = sp_get_selector_classes(tok); + if (!clases.empty()) { + _removeClass(obj, tok, true); + } + auto i = tok.find(row[_mColumns._colSelector]); + if (i == std::string::npos) { + selector = selector.empty() ? tok : selector + "," + tok; + } + } + REMOVE_SPACES(selector); + if (selector.empty()) { + _store->erase(parent); + + } else { + _store->erase(row); + parent[_mColumns._colSelector] = selector; + parent[_mColumns._colExpand] = true; + parent[_mColumns._colObj] = _getObjVec(selector); + } + } + _updating = false; + + // Add entry to style element + _writeStyleElement(); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } +} + + +/** + * @param sel + * @return This function returns a comma separated list of ids for objects in input vector. + * It is used in creating an 'id' selector. It relies on objects having 'id's. + */ +Glib::ustring SelectorsDialog::_getIdList(std::vector sel) +{ + g_debug("SelectorsDialog::_getIdList"); + + Glib::ustring str; + for (auto& obj: sel) { + char const *id = obj->getId(); + if (id) { + if (!str.empty()) { + str.append(", "); + } + str.append("#").append(id); + } + } + return str; +} + +/** + * @param selector: a valid CSS selector string. + * @return objVec: a vector of pointers to SPObject's the selector matches. + * Return a vector of all objects that selector matches. + */ +std::vector SelectorsDialog::_getObjVec(Glib::ustring selector) +{ + + g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str()); + + g_assert(selector.find(";") == Glib::ustring::npos); + + return getDesktop()->getDocument()->getObjectsBySelector(selector); +} + + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(const std::vector &objVec, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + for (auto& obj: objVec) { + _insertClass(obj, className); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + Glib::ustring classAttr = Glib::ustring(""); + if (obj->getRepr()->attribute("class")) { + classAttr = obj->getRepr()->attribute("class"); + } + std::vector tokens = Glib::Regex::split_simple("[.]+", className); + std::sort(tokens.begin(), tokens.end()); + tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end()); + std::vector tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr); + for (auto tok : tokens) { + bool exist = false; + for (auto &tokenplus : tokensplus) { + if (tokenplus == tok) { + exist = true; + } + } + if (!exist) { + classAttr = classAttr.empty() ? tok : classAttr + " " + tok; + } + } + obj->getRepr()->setAttribute("class", classAttr); +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(const std::vector &objVec, const Glib::ustring &className, bool all) +{ + g_debug("SelectorsDialog::_removeClass"); + + for (auto &obj : objVec) { + _removeClass(obj, className, all); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "." +{ + g_debug("SelectorsDialog::_removeClass"); + + if (obj->getRepr()->attribute("class")) { + std::vector tokens = Glib::Regex::split_simple("[.]+", className); + Glib::ustring classAttr = obj->getRepr()->attribute("class"); + Glib::ustring classAttrRestore = classAttr; + bool notfound = false; + for (auto tok : tokens) { + auto i = classAttr.find(tok); + if (i != std::string::npos) { + classAttr.erase(i, tok.length()); + } else { + notfound = true; + } + } + if (all && notfound) { + classAttr = classAttrRestore; + } + REMOVE_SPACES(classAttr); + if (classAttr.empty()) { + obj->getRepr()->removeAttribute("class"); + } else { + obj->getRepr()->setAttribute("class", classAttr); + } + } +} + + +/** + * @param eventX + * @param eventY + * This function selects objects in the drawing corresponding to the selector + * selected in the treeview. + */ +void SelectorsDialog::_selectObjects(int eventX, int eventY) +{ + g_debug("SelectorsDialog::_selectObjects: %d, %d", eventX, eventY); + Gtk::TreeViewColumn *col = _treeView.get_column(1); + Gtk::TreeModel::Path path; + int x2 = 0; + int y2 = 0; + // To do: We should be able to do this via passing in row. + if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) { + if (_lastpath.size() && _lastpath == path) { + return; + } + if (col == _treeView.get_column(1) && x2 > 25) { + getDesktop()->selection->clear(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Gtk::TreeModel::Children children = row.children(); + if (children.empty() || children.size() == 1) { + _del.show(); + } + std::vector objVec = row[_mColumns._colObj]; + + for (auto obj : objVec) { + getDesktop()->selection->add(obj); + } + } + _lastpath = path; + } + } +} + +/** + * This function opens a dialog to add a selector. The dialog is prefilled + * with an 'id' selector containing a list of the id's of selected objects + * or with a 'class' selector if no objects are selected. + */ +void SelectorsDialog::_addSelector() +{ + g_debug("SelectorsDialog::_addSelector: Entrance"); + _scroollock = true; + // Store list of selected elements on desktop (not to be confused with selector). + Inkscape::Selection* selection = getDesktop()->getSelection(); + std::vector objVec( selection->objects().begin(), + selection->objects().end() ); + + // ==== Create popup dialog ==== + Gtk::Dialog *textDialogPtr = new Gtk::Dialog(); + textDialogPtr->property_modal() = true; + textDialogPtr->property_title() = _("CSS selector"); + textDialogPtr->property_window_position() = Gtk::WIN_POS_CENTER_ON_PARENT; + textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL); + textDialogPtr->add_button(_("Add"), Gtk::RESPONSE_OK); + + Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() ); + textEditPtr->signal_activate().connect( + sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr)); + textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK); + + Gtk::Label *textLabelPtr = manage(new Gtk::Label(_("Invalid CSS selector."))); + textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK); + + /** + * By default, the entrybox contains 'Class1' as text. However, if object(s) + * is(are) selected and user clicks '+' at the bottom of dialog, the + * entrybox will have the id(s) of the selected objects as text. + */ + if (getDesktop()->getSelection()->isEmpty()) { + textEditPtr->set_text(".Class1"); + } else { + textEditPtr->set_text(_getIdList(objVec)); + } + + Gtk::Requisition sreq1, sreq2; + textDialogPtr->get_preferred_size(sreq1, sreq2); + int minWidth = 200; + int minHeight = 100; + minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth ); + minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight); + textDialogPtr->set_size_request(minWidth, minHeight); + textEditPtr->show(); + textLabelPtr->hide(); + textDialogPtr->show(); + + + // ==== Get response ==== + int result = -1; + bool invalid = true; + Glib::ustring selectorValue; + Glib::ustring originalValue; + while (invalid) { + result = textDialogPtr->run(); + if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc. + textDialogPtr->hide(); + delete textDialogPtr; + return; + } + /** + * @brief selectorName + * This string stores selector name. The text from entrybox is saved as name + * for selector. If the entrybox is empty, the text (thus selectorName) is + * set to ".Class1" + */ + originalValue = Glib::ustring(textEditPtr->get_text()); + selectorValue = _style_dialog->fixCSSSelectors(originalValue); + _del.show(); + if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) { + textLabelPtr->show(); + } else { + invalid = false; + } + } + delete textDialogPtr; + // ==== Handle response ==== + // If class selector, add selector name to class attribute for each object + REMOVE_SPACES(selectorValue); + if (originalValue.find("@import ") != std::string::npos) { + std::vector objVecEmpty; + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colSelector] = originalValue; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = objVecEmpty; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } else { + std::vector tokens = Glib::Regex::split_simple("[,]+", selectorValue); + for (auto &obj : objVec) { + for (auto tok : tokens) { + Glib::ustring clases = sp_get_selector_classes(tok); + if (clases.empty()) { + continue; + } + _insertClass(obj, clases); + std::vector currentobjs = _getObjVec(selectorValue); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (currentobj == obj) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + objVec = _getObjVec(selectorValue); + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colExpand] = true; + row[_mColumns._colType] = SELECTOR; + row[_mColumns._colSelector] = selectorValue; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + for (auto &obj : objVec) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = std::vector(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + // Add entry to style element + _writeStyleElement(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + +/** + * This function deletes selector when '-' at the bottom is clicked. + * Note: If deleting a class selector, class attributes are NOT changed. + */ +void SelectorsDialog::_delSelector() +{ + g_debug("SelectorsDialog::_delSelector"); + + _scroollock = true; + Glib::RefPtr refTreeSelection = _treeView.get_selection(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); + if (iter) { + _vscrool(); + Gtk::TreeModel::Row row = *iter; + if (row.children().size() > 2) { + return; + } + _updating = true; + _store->erase(iter); + _updating = false; + _writeStyleElement(); + _del.hide(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } +} + +/** + * @param event + * @return + * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in + * front of a child object is clicked. In the first case, the selected objects on the desktop (if + * any) are added as children of the selector in the treeview. In the latter case, the object + * corresponding to the row is removed from the selector. + */ +bool SelectorsDialog::_handleButtonEvent(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_handleButtonEvent: Entrance"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + _scroollock = true; + Gtk::TreeViewColumn *col = nullptr; + Gtk::TreeModel::Path path; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + + if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) { + if (col == _treeView.get_column(0)) { + _vscrool(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (!row.parent()) { + _addToSelector(row); + } else { + _removeFromSelector(row); + } + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } + } + } + return false; +} + +// ------------------------------------------------------------------- + +class PropertyData +{ +public: + PropertyData() = default;; + PropertyData(Glib::ustring name) : _name(std::move(name)) {}; + + void _setSheetValue(Glib::ustring value) { _sheetValue = value; }; + void _setAttrValue(Glib::ustring value) { _attrValue = value; }; + Glib::ustring _getName() { return _name; }; + Glib::ustring _getSheetValue() { return _sheetValue; }; + Glib::ustring _getAttrValue() { return _attrValue; }; + +private: + Glib::ustring _name; + Glib::ustring _sheetValue; + Glib::ustring _attrValue; +}; + +// ------------------------------------------------------------------- + + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void SelectorsDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) +{ + g_debug("SelectorsDialog::handleDocumentReplaced()"); + + _selection_changed_connection.disconnect(); + + _updateWatchers(desktop); + + if (!desktop) + return; + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + _readStyleElement(); + _selectRow(); +} + + +/* + * When a dialog is floating, it is connected to the active desktop. + */ +void SelectorsDialog::_handleDesktopChanged(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::handleDesktopReplaced()"); + + if (getDesktop() == desktop) { + // This will happen after construction of dialog. We've already + // set up signals so just return. + return; + } + + _selection_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + + setDesktop( desktop ); + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _updateWatchers(desktop); + _readStyleElement(); + _selectRow(); +} + + +/* + * Handle a change in which objects are selected in a document. + */ +void SelectorsDialog::_handleSelectionChanged() +{ + g_debug("SelectorsDialog::_handleSelectionChanged()"); + _lastpath.clear(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _readStyleElement(); + _selectRow(); +} + + +/** + * @param event + * This function detects single or double click on a selector in any row. Clicking + * on a selector selects the matching objects on the desktop. A double click will + * in addition open the CSS dialog. + */ +void SelectorsDialog::_buttonEventsSelectObjs(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_buttonEventsSelectObjs"); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + _updating = true; + _del.show(); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + int x = static_cast(event->x); + int y = static_cast(event->y); + _selectObjects(x, y); + } + _updating = false; +} + + +/** + * This function selects the row in treeview corresponding to an object selected + * in the drawing. If more than one row matches, the first is chosen. + */ +void SelectorsDialog::_selectRow() +{ + _scroollock = true; + g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false")); + _del.hide(); + std::vector selectedrows = _treeView.get_selection()->get_selected_rows(); + if (selectedrows.size() == 1) { + Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]); + if (!row->parent() && row->children().size() < 2) { + _del.show(); + } + if (!row->parent()) { + _style_dialog->setCurrentSelector(row[_mColumns._colSelector]); + } + } else if (selectedrows.size() == 0) { + _del.show(); + } + if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog. + + _treeView.get_selection()->unselect_all(); + Gtk::TreeModel::Children children = _store->children(); + Inkscape::Selection* selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (!selection->isEmpty()) { + obj = selection->objects().back(); + } else { + _style_dialog->setCurrentSelector(""); + } + for (auto row : children) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + subrow[_mColumns._colSelected] = 400; + } + } + for (auto obj : selection->items()) { + for (auto row : children) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + std::vector objVec = subrow[_mColumns._colObj]; + if (obj == objVec[0]) { + _treeView.get_selection()->select(row); + row[_mColumns._colVisible] = true; + subrow[_mColumns._colSelected] = 700; + } + } + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + } + for (auto row : children) { + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +/** + * @param btn + * @param iconName + * @param tooltip + * Set the style of '+' and '-' buttons at the bottom of dialog. + */ +void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip) +{ + g_debug("SelectorsDialog::_styleButton"); + + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn.add(*manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + + +} // 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 : diff --git a/src/ui/dialog/selectorsdialog.h b/src/ui/dialog/selectorsdialog.h new file mode 100644 index 0000000..2c0e363 --- /dev/null +++ b/src/ui/dialog/selectorsdialog.h @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 + * Copyright (C) Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SELECTORSDIALOG_H +#define SELECTORSDIALOG_H + +#include "ui/dialog/desktop-tracker.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/styledialog.h" +#include "ui/widget/panel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xml/helper-observer.h" + +#include +#include + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * @brief The SelectorsDialog class + * A list of CSS selectors will show up in this dialog. This dialog allows one to + * add and delete selectors. Elements can be added to and removed from the selectors + * in the dialog. Selection of any selector row selects the matching objects in + * the drawing and vice-versa. (Only simple selectors supported for now.) + * + * This class must keep two things in sync: + * 1. The text node of the style element. + * 2. The Gtk::TreeModel. + */ +class SelectorsDialog : public Widget::Panel { + + public: + ~SelectorsDialog() override; + // No default constructor, noncopyable, nonassignable + SelectorsDialog(); + SelectorsDialog(SelectorsDialog const &d) = delete; + SelectorsDialog operator=(SelectorsDialog const &d) = delete; + static SelectorsDialog &getInstance() { return *new SelectorsDialog(); } + + private: + // Monitor