summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
commit35a96bde514a8897f6f0fcc41c5833bf63df2e2a (patch)
tree657d15a03cc46bd099fc2c6546a7a4ad43815d9f /src/ui
parentInitial commit. (diff)
downloadinkscape-35a96bde514a8897f6f0fcc41c5833bf63df2e2a.tar.xz
inkscape-35a96bde514a8897f6f0fcc41c5833bf63df2e2a.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/CMakeLists.txt477
-rw-r--r--src/ui/README21
-rw-r--r--src/ui/cache/README3
-rw-r--r--src/ui/cache/svg_preview_cache.cpp142
-rw-r--r--src/ui/cache/svg_preview_cache.h65
-rw-r--r--src/ui/clipboard.cpp1682
-rw-r--r--src/ui/clipboard.h75
-rw-r--r--src/ui/contextmenu.cpp1008
-rw-r--r--src/ui/contextmenu.h225
-rw-r--r--src/ui/control-manager.cpp509
-rw-r--r--src/ui/control-manager.h96
-rw-r--r--src/ui/control-types.h58
-rw-r--r--src/ui/desktop/README27
-rw-r--r--src/ui/desktop/menubar.cpp635
-rw-r--r--src/ui/desktop/menubar.h48
-rw-r--r--src/ui/dialog-events.cpp244
-rw-r--r--src/ui/dialog-events.h76
-rw-r--r--src/ui/dialog/aboutbox.cpp224
-rw-r--r--src/ui/dialog/aboutbox.h66
-rw-r--r--src/ui/dialog/align-and-distribute.cpp1339
-rw-r--r--src/ui/dialog/align-and-distribute.h223
-rw-r--r--src/ui/dialog/arrange-tab.h55
-rw-r--r--src/ui/dialog/attrdialog.cpp682
-rw-r--r--src/ui/dialog/attrdialog.h128
-rw-r--r--src/ui/dialog/behavior.h104
-rw-r--r--src/ui/dialog/calligraphic-profile-rename.cpp142
-rw-r--r--src/ui/dialog/calligraphic-profile-rename.h87
-rw-r--r--src/ui/dialog/clonetiler.cpp2834
-rw-r--r--src/ui/dialog/clonetiler.h226
-rw-r--r--src/ui/dialog/color-item.cpp751
-rw-r--r--src/ui/dialog/color-item.h130
-rw-r--r--src/ui/dialog/debug.cpp259
-rw-r--r--src/ui/dialog/debug.h101
-rw-r--r--src/ui/dialog/desktop-tracker.cpp154
-rw-r--r--src/ui/dialog/desktop-tracker.h69
-rw-r--r--src/ui/dialog/dialog-manager.cpp310
-rw-r--r--src/ui/dialog/dialog-manager.h74
-rw-r--r--src/ui/dialog/dialog.cpp370
-rw-r--r--src/ui/dialog/dialog.h179
-rw-r--r--src/ui/dialog/dock-behavior.cpp297
-rw-r--r--src/ui/dialog/dock-behavior.h104
-rw-r--r--src/ui/dialog/document-metadata.cpp223
-rw-r--r--src/ui/dialog/document-metadata.h86
-rw-r--r--src/ui/dialog/document-properties.cpp1708
-rw-r--r--src/ui/dialog/document-properties.h262
-rw-r--r--src/ui/dialog/export.cpp1948
-rw-r--r--src/ui/dialog/export.h367
-rw-r--r--src/ui/dialog/extension-editor.cpp221
-rw-r--r--src/ui/dialog/extension-editor.h93
-rw-r--r--src/ui/dialog/extensions.cpp120
-rw-r--r--src/ui/dialog/extensions.h57
-rw-r--r--src/ui/dialog/filedialog.cpp201
-rw-r--r--src/ui/dialog/filedialog.h254
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.cpp867
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.h313
-rw-r--r--src/ui/dialog/filedialogimpl-win32.cpp1937
-rw-r--r--src/ui/dialog/filedialogimpl-win32.h393
-rw-r--r--src/ui/dialog/fill-and-stroke.cpp207
-rw-r--r--src/ui/dialog/fill-and-stroke.h100
-rw-r--r--src/ui/dialog/filter-editor.cpp120
-rw-r--r--src/ui/dialog/filter-editor.h53
-rw-r--r--src/ui/dialog/filter-effects-dialog.cpp3114
-rw-r--r--src/ui/dialog/filter-effects-dialog.h346
-rw-r--r--src/ui/dialog/find.cpp1076
-rw-r--r--src/ui/dialog/find.h320
-rw-r--r--src/ui/dialog/floating-behavior.cpp168
-rw-r--r--src/ui/dialog/floating-behavior.h92
-rw-r--r--src/ui/dialog/font-substitution.cpp271
-rw-r--r--src/ui/dialog/font-substitution.h58
-rw-r--r--src/ui/dialog/glyphs.cpp822
-rw-r--r--src/ui/dialog/glyphs.h97
-rw-r--r--src/ui/dialog/grid-arrange-tab.cpp776
-rw-r--r--src/ui/dialog/grid-arrange-tab.h148
-rw-r--r--src/ui/dialog/guides.cpp358
-rw-r--r--src/ui/dialog/guides.h103
-rw-r--r--src/ui/dialog/icon-preview.cpp682
-rw-r--r--src/ui/dialog/icon-preview.h119
-rw-r--r--src/ui/dialog/inkscape-preferences.cpp2772
-rw-r--r--src/ui/dialog/inkscape-preferences.h620
-rw-r--r--src/ui/dialog/input.cpp1792
-rw-r--r--src/ui/dialog/input.h47
-rw-r--r--src/ui/dialog/knot-properties.cpp207
-rw-r--r--src/ui/dialog/knot-properties.h97
-rw-r--r--src/ui/dialog/layer-properties.cpp424
-rw-r--r--src/ui/dialog/layer-properties.h177
-rw-r--r--src/ui/dialog/layers.cpp1004
-rw-r--r--src/ui/dialog/layers.h152
-rw-r--r--src/ui/dialog/livepatheffect-add.cpp933
-rw-r--r--src/ui/dialog/livepatheffect-add.h147
-rw-r--r--src/ui/dialog/livepatheffect-editor.cpp634
-rw-r--r--src/ui/dialog/livepatheffect-editor.h152
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.cpp276
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.h114
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.cpp199
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.h92
-rw-r--r--src/ui/dialog/memory.cpp247
-rw-r--r--src/ui/dialog/memory.h54
-rw-r--r--src/ui/dialog/messages.cpp216
-rw-r--r--src/ui/dialog/messages.h103
-rw-r--r--src/ui/dialog/new-from-template.cpp72
-rw-r--r--src/ui/dialog/new-from-template.h45
-rw-r--r--src/ui/dialog/object-attributes.cpp218
-rw-r--r--src/ui/dialog/object-attributes.h123
-rw-r--r--src/ui/dialog/object-properties.cpp605
-rw-r--r--src/ui/dialog/object-properties.h148
-rw-r--r--src/ui/dialog/objects.cpp2363
-rw-r--r--src/ui/dialog/objects.h279
-rw-r--r--src/ui/dialog/paint-servers.cpp505
-rw-r--r--src/ui/dialog/paint-servers.h88
-rw-r--r--src/ui/dialog/panel-dialog.h208
-rw-r--r--src/ui/dialog/polar-arrange-tab.cpp408
-rw-r--r--src/ui/dialog/polar-arrange-tab.h104
-rw-r--r--src/ui/dialog/print-colors-preview-dialog.cpp103
-rw-r--r--src/ui/dialog/print-colors-preview-dialog.h48
-rw-r--r--src/ui/dialog/print.cpp263
-rw-r--r--src/ui/dialog/print.h80
-rw-r--r--src/ui/dialog/save-template-dialog.cpp101
-rw-r--r--src/ui/dialog/save-template-dialog.h60
-rw-r--r--src/ui/dialog/selectorsdialog.cpp1508
-rw-r--r--src/ui/dialog/selectorsdialog.h209
-rw-r--r--src/ui/dialog/spellcheck.cpp815
-rw-r--r--src/ui/dialog/spellcheck.h301
-rw-r--r--src/ui/dialog/styledialog.cpp1690
-rw-r--r--src/ui/dialog/styledialog.h208
-rw-r--r--src/ui/dialog/svg-fonts-dialog.cpp1067
-rw-r--r--src/ui/dialog/svg-fonts-dialog.h283
-rw-r--r--src/ui/dialog/svg-preview.cpp476
-rw-r--r--src/ui/dialog/svg-preview.h123
-rw-r--r--src/ui/dialog/swatches.cpp1418
-rw-r--r--src/ui/dialog/swatches.h110
-rw-r--r--src/ui/dialog/symbols.cpp1403
-rw-r--r--src/ui/dialog/symbols.h183
-rw-r--r--src/ui/dialog/tags.cpp1125
-rw-r--r--src/ui/dialog/tags.h174
-rw-r--r--src/ui/dialog/template-load-tab.cpp337
-rw-r--r--src/ui/dialog/template-load-tab.h119
-rw-r--r--src/ui/dialog/template-widget.cpp152
-rw-r--r--src/ui/dialog/template-widget.h51
-rw-r--r--src/ui/dialog/text-edit.cpp585
-rw-r--r--src/ui/dialog/text-edit.h217
-rw-r--r--src/ui/dialog/tile.cpp81
-rw-r--r--src/ui/dialog/tile.h80
-rw-r--r--src/ui/dialog/tracedialog.cpp380
-rw-r--r--src/ui/dialog/tracedialog.h69
-rw-r--r--src/ui/dialog/transformation.cpp1171
-rw-r--r--src/ui/dialog/transformation.h258
-rw-r--r--src/ui/dialog/undo-history.cpp406
-rw-r--r--src/ui/dialog/undo-history.h179
-rw-r--r--src/ui/dialog/xml-tree.cpp968
-rw-r--r--src/ui/dialog/xml-tree.h265
-rw-r--r--src/ui/drag-and-drop.cpp534
-rw-r--r--src/ui/drag-and-drop.h51
-rw-r--r--src/ui/draw-anchor.cpp108
-rw-r--r--src/ui/draw-anchor.h61
-rw-r--r--src/ui/event-debug.h122
-rw-r--r--src/ui/icon-loader.cpp137
-rw-r--r--src/ui/icon-loader.h29
-rw-r--r--src/ui/icon-names.h32
-rw-r--r--src/ui/interface.cpp268
-rw-r--r--src/ui/interface.h76
-rw-r--r--src/ui/monitor.cpp68
-rw-r--r--src/ui/monitor.h38
-rw-r--r--src/ui/pixmaps/README2
-rw-r--r--src/ui/pixmaps/cursor-3dbox.xpm38
-rw-r--r--src/ui/pixmaps/cursor-adj-a.xpm38
-rw-r--r--src/ui/pixmaps/cursor-adj-h.xpm38
-rw-r--r--src/ui/pixmaps/cursor-adj-l.xpm38
-rw-r--r--src/ui/pixmaps/cursor-adj-s.xpm38
-rw-r--r--src/ui/pixmaps/cursor-calligraphy.xpm38
-rw-r--r--src/ui/pixmaps/cursor-connector.xpm38
-rw-r--r--src/ui/pixmaps/cursor-crosshairs.xpm38
-rw-r--r--src/ui/pixmaps/cursor-dropper-f.xpm39
-rw-r--r--src/ui/pixmaps/cursor-dropper-s.xpm39
-rw-r--r--src/ui/pixmaps/cursor-dropping-f.xpm39
-rw-r--r--src/ui/pixmaps/cursor-dropping-s.xpm39
-rw-r--r--src/ui/pixmaps/cursor-ellipse.xpm40
-rw-r--r--src/ui/pixmaps/cursor-eraser.xpm38
-rw-r--r--src/ui/pixmaps/cursor-gradient-add.xpm38
-rw-r--r--src/ui/pixmaps/cursor-gradient.xpm38
-rw-r--r--src/ui/pixmaps/cursor-measure.xpm38
-rw-r--r--src/ui/pixmaps/cursor-node-d.xpm38
-rw-r--r--src/ui/pixmaps/cursor-node.xpm38
-rw-r--r--src/ui/pixmaps/cursor-paintbucket.xpm38
-rw-r--r--src/ui/pixmaps/cursor-pen.xpm38
-rw-r--r--src/ui/pixmaps/cursor-pencil.xpm38
-rw-r--r--src/ui/pixmaps/cursor-rect.xpm40
-rw-r--r--src/ui/pixmaps/cursor-select-d.xpm38
-rw-r--r--src/ui/pixmaps/cursor-select-m.xpm38
-rw-r--r--src/ui/pixmaps/cursor-select.xpm38
-rw-r--r--src/ui/pixmaps/cursor-spiral.xpm38
-rw-r--r--src/ui/pixmaps/cursor-spray-move.xpm38
-rw-r--r--src/ui/pixmaps/cursor-spray.xpm38
-rw-r--r--src/ui/pixmaps/cursor-star.xpm40
-rw-r--r--src/ui/pixmaps/cursor-text-insert.xpm38
-rw-r--r--src/ui/pixmaps/cursor-text.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-attract.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-color.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-less.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-more.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-move-in.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-move-jitter.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-move-out.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-move.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-push.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-repel.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-roughen.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-scale-down.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-scale-up.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-thicken.xpm38
-rw-r--r--src/ui/pixmaps/cursor-tweak-thin.xpm38
-rw-r--r--src/ui/pixmaps/cursor-zoom-out.xpm38
-rw-r--r--src/ui/pixmaps/cursor-zoom.xpm38
-rw-r--r--src/ui/pixmaps/handles.xpm159
-rw-r--r--src/ui/pref-pusher.cpp70
-rw-r--r--src/ui/pref-pusher.h80
-rw-r--r--src/ui/previewable.h64
-rw-r--r--src/ui/previewholder.cpp447
-rw-r--r--src/ui/previewholder.h90
-rw-r--r--src/ui/selected-color.cpp159
-rw-r--r--src/ui/selected-color.h99
-rw-r--r--src/ui/shape-editor-knotholders.cpp1980
-rw-r--r--src/ui/shape-editor.cpp218
-rw-r--r--src/ui/shape-editor.h71
-rw-r--r--src/ui/simple-pref-pusher.cpp49
-rw-r--r--src/ui/simple-pref-pusher.h65
-rw-r--r--src/ui/tool-factory.cpp101
-rw-r--r--src/ui/tool-factory.h43
-rw-r--r--src/ui/tool/commit-events.h52
-rw-r--r--src/ui/tool/control-point-selection.cpp773
-rw-r--r--src/ui/tool/control-point-selection.h178
-rw-r--r--src/ui/tool/control-point.cpp637
-rw-r--r--src/ui/tool/control-point.h411
-rw-r--r--src/ui/tool/curve-drag-point.cpp231
-rw-r--r--src/ui/tool/curve-drag-point.h77
-rw-r--r--src/ui/tool/event-utils.cpp162
-rw-r--r--src/ui/tool/event-utils.h133
-rw-r--r--src/ui/tool/manipulator.cpp90
-rw-r--r--src/ui/tool/manipulator.h174
-rw-r--r--src/ui/tool/modifier-tracker.cpp94
-rw-r--r--src/ui/tool/modifier-tracker.h55
-rw-r--r--src/ui/tool/multi-path-manipulator.cpp888
-rw-r--r--src/ui/tool/multi-path-manipulator.h154
-rw-r--r--src/ui/tool/node-types.h49
-rw-r--r--src/ui/tool/node.cpp1924
-rw-r--r--src/ui/tool/node.h527
-rw-r--r--src/ui/tool/path-manipulator.cpp1756
-rw-r--r--src/ui/tool/path-manipulator.h180
-rw-r--r--src/ui/tool/selectable-control-point.cpp147
-rw-r--r--src/ui/tool/selectable-control-point.h78
-rw-r--r--src/ui/tool/selector.cpp150
-rw-r--r--src/ui/tool/selector.h60
-rw-r--r--src/ui/tool/shape-record.h62
-rw-r--r--src/ui/tool/transform-handle-set.cpp867
-rw-r--r--src/ui/tool/transform-handle-set.h142
-rw-r--r--src/ui/toolbar/arc-toolbar.cpp561
-rw-r--r--src/ui/toolbar/arc-toolbar.h116
-rw-r--r--src/ui/toolbar/box3d-toolbar.cpp418
-rw-r--r--src/ui/toolbar/box3d-toolbar.h106
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.cpp599
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.h103
-rw-r--r--src/ui/toolbar/connector-toolbar.cpp437
-rw-r--r--src/ui/toolbar/connector-toolbar.h93
-rw-r--r--src/ui/toolbar/dropper-toolbar.cpp116
-rw-r--r--src/ui/toolbar/dropper-toolbar.h70
-rw-r--r--src/ui/toolbar/eraser-toolbar.cpp335
-rw-r--r--src/ui/toolbar/eraser-toolbar.h95
-rw-r--r--src/ui/toolbar/gradient-toolbar.cpp1178
-rw-r--r--src/ui/toolbar/gradient-toolbar.h102
-rw-r--r--src/ui/toolbar/lpe-toolbar.cpp418
-rw-r--r--src/ui/toolbar/lpe-toolbar.h101
-rw-r--r--src/ui/toolbar/measure-toolbar.cpp452
-rw-r--r--src/ui/toolbar/measure-toolbar.h91
-rw-r--r--src/ui/toolbar/mesh-toolbar.cpp621
-rw-r--r--src/ui/toolbar/mesh-toolbar.h97
-rw-r--r--src/ui/toolbar/node-toolbar.cpp651
-rw-r--r--src/ui/toolbar/node-toolbar.h115
-rw-r--r--src/ui/toolbar/paintbucket-toolbar.cpp221
-rw-r--r--src/ui/toolbar/paintbucket-toolbar.h72
-rw-r--r--src/ui/toolbar/pencil-toolbar.cpp622
-rw-r--r--src/ui/toolbar/pencil-toolbar.h99
-rw-r--r--src/ui/toolbar/rect-toolbar.cpp407
-rw-r--r--src/ui/toolbar/rect-toolbar.h113
-rw-r--r--src/ui/toolbar/select-toolbar.cpp508
-rw-r--r--src/ui/toolbar/select-toolbar.h82
-rw-r--r--src/ui/toolbar/snap-toolbar.cpp402
-rw-r--r--src/ui/toolbar/snap-toolbar.h70
-rw-r--r--src/ui/toolbar/spiral-toolbar.cpp304
-rw-r--r--src/ui/toolbar/spiral-toolbar.h98
-rw-r--r--src/ui/toolbar/spray-toolbar.cpp550
-rw-r--r--src/ui/toolbar/spray-toolbar.h107
-rw-r--r--src/ui/toolbar/star-toolbar.cpp564
-rw-r--r--src/ui/toolbar/star-toolbar.h108
-rw-r--r--src/ui/toolbar/text-toolbar.cpp2540
-rw-r--r--src/ui/toolbar/text-toolbar.h149
-rw-r--r--src/ui/toolbar/toolbar.cpp102
-rw-r--r--src/ui/toolbar/toolbar.h67
-rw-r--r--src/ui/toolbar/tweak-toolbar.cpp347
-rw-r--r--src/ui/toolbar/tweak-toolbar.h89
-rw-r--r--src/ui/toolbar/zoom-toolbar.cpp85
-rw-r--r--src/ui/toolbar/zoom-toolbar.h62
-rw-r--r--src/ui/tools-switch.cpp195
-rw-r--r--src/ui/tools-switch.h65
-rw-r--r--src/ui/tools/arc-tool.cpp488
-rw-r--r--src/ui/tools/arc-tool.h83
-rw-r--r--src/ui/tools/box3d-tool.cpp614
-rw-r--r--src/ui/tools/box3d-tool.h109
-rw-r--r--src/ui/tools/calligraphic-tool.cpp1203
-rw-r--r--src/ui/tools/calligraphic-tool.h103
-rw-r--r--src/ui/tools/connector-tool.cpp1393
-rw-r--r--src/ui/tools/connector-tool.h168
-rw-r--r--src/ui/tools/dropper-tool.cpp414
-rw-r--r--src/ui/tools/dropper-tool.h87
-rw-r--r--src/ui/tools/dynamic-base.cpp166
-rw-r--r--src/ui/tools/dynamic-base.h130
-rw-r--r--src/ui/tools/eraser-tool.cpp1119
-rw-r--r--src/ui/tools/eraser-tool.h84
-rw-r--r--src/ui/tools/flood-tool.cpp1254
-rw-r--r--src/ui/tools/flood-tool.h72
-rw-r--r--src/ui/tools/freehand-base.cpp1112
-rw-r--r--src/ui/tools/freehand-base.h164
-rw-r--r--src/ui/tools/gradient-tool.cpp932
-rw-r--r--src/ui/tools/gradient-tool.h77
-rw-r--r--src/ui/tools/lpe-tool.cpp494
-rw-r--r--src/ui/tools/lpe-tool.h100
-rw-r--r--src/ui/tools/measure-tool.cpp1466
-rw-r--r--src/ui/tools/measure-tool.h110
-rw-r--r--src/ui/tools/mesh-tool.cpp1098
-rw-r--r--src/ui/tools/mesh-tool.h87
-rw-r--r--src/ui/tools/node-tool.cpp848
-rw-r--r--src/ui/tools/node-tool.h118
-rw-r--r--src/ui/tools/pen-tool.cpp2104
-rw-r--r--src/ui/tools/pen-tool.h166
-rw-r--r--src/ui/tools/pencil-tool.cpp1239
-rw-r--r--src/ui/tools/pencil-tool.h104
-rw-r--r--src/ui/tools/rect-tool.cpp503
-rw-r--r--src/ui/tools/rect-tool.h64
-rw-r--r--src/ui/tools/select-tool.cpp1171
-rw-r--r--src/ui/tools/select-tool.h72
-rw-r--r--src/ui/tools/spiral-tool.cpp444
-rw-r--r--src/ui/tools/spiral-tool.h65
-rw-r--r--src/ui/tools/spray-tool.cpp1533
-rw-r--r--src/ui/tools/spray-tool.h151
-rw-r--r--src/ui/tools/star-tool.cpp461
-rw-r--r--src/ui/tools/star-tool.h75
-rw-r--r--src/ui/tools/text-tool.cpp1897
-rw-r--r--src/ui/tools/text-tool.h108
-rw-r--r--src/ui/tools/tool-base.cpp1630
-rw-r--r--src/ui/tools/tool-base.h284
-rw-r--r--src/ui/tools/tweak-tool.cpp1535
-rw-r--r--src/ui/tools/tweak-tool.h107
-rw-r--r--src/ui/tools/zoom-tool.cpp243
-rw-r--r--src/ui/tools/zoom-tool.h48
-rw-r--r--src/ui/util.cpp38
-rw-r--r--src/ui/util.h31
-rw-r--r--src/ui/uxmanager.cpp246
-rw-r--r--src/ui/uxmanager.h61
-rw-r--r--src/ui/view/README51
-rw-r--r--src/ui/view/edit-widget-interface.h178
-rw-r--r--src/ui/view/svg-view-widget.cpp266
-rw-r--r--src/ui/view/svg-view-widget.h89
-rw-r--r--src/ui/view/view-widget.cpp102
-rw-r--r--src/ui/view/view-widget.h107
-rw-r--r--src/ui/view/view.cpp138
-rw-r--r--src/ui/view/view.h148
-rw-r--r--src/ui/widget/alignment-selector.cpp78
-rw-r--r--src/ui/widget/alignment-selector.h53
-rw-r--r--src/ui/widget/anchor-selector.cpp97
-rw-r--r--src/ui/widget/anchor-selector.h62
-rw-r--r--src/ui/widget/attr-widget.h185
-rw-r--r--src/ui/widget/button.cpp273
-rw-r--r--src/ui/widget/button.h89
-rw-r--r--src/ui/widget/clipmaskicon.cpp123
-rw-r--r--src/ui/widget/clipmaskicon.h86
-rw-r--r--src/ui/widget/color-entry.cpp157
-rw-r--r--src/ui/widget/color-entry.h58
-rw-r--r--src/ui/widget/color-icc-selector.cpp1074
-rw-r--r--src/ui/widget/color-icc-selector.h75
-rw-r--r--src/ui/widget/color-notebook.cpp341
-rw-r--r--src/ui/widget/color-notebook.h89
-rw-r--r--src/ui/widget/color-picker.cpp144
-rw-r--r--src/ui/widget/color-picker.h114
-rw-r--r--src/ui/widget/color-preview.cpp171
-rw-r--r--src/ui/widget/color-preview.h57
-rw-r--r--src/ui/widget/color-scales.cpp736
-rw-r--r--src/ui/widget/color-scales.h110
-rw-r--r--src/ui/widget/color-slider.cpp536
-rw-r--r--src/ui/widget/color-slider.h93
-rw-r--r--src/ui/widget/color-wheel-selector.cpp237
-rw-r--r--src/ui/widget/color-wheel-selector.h83
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.cpp691
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.h157
-rw-r--r--src/ui/widget/combo-enums.h224
-rw-r--r--src/ui/widget/combo-tool-item.cpp290
-rw-r--r--src/ui/widget/combo-tool-item.h136
-rw-r--r--src/ui/widget/dash-selector.cpp309
-rw-r--r--src/ui/widget/dash-selector.h112
-rw-r--r--src/ui/widget/dock-item.cpp528
-rw-r--r--src/ui/widget/dock-item.h159
-rw-r--r--src/ui/widget/dock.cpp305
-rw-r--r--src/ui/widget/dock.h108
-rw-r--r--src/ui/widget/entity-entry.cpp207
-rw-r--r--src/ui/widget/entity-entry.h85
-rw-r--r--src/ui/widget/entry.cpp30
-rw-r--r--src/ui/widget/entry.h45
-rw-r--r--src/ui/widget/filter-effect-chooser.cpp194
-rw-r--r--src/ui/widget/filter-effect-chooser.h95
-rw-r--r--src/ui/widget/font-button.cpp58
-rw-r--r--src/ui/widget/font-button.h63
-rw-r--r--src/ui/widget/font-selector-toolbar.cpp301
-rw-r--r--src/ui/widget/font-selector-toolbar.h120
-rw-r--r--src/ui/widget/font-selector.cpp450
-rw-r--r--src/ui/widget/font-selector.h163
-rw-r--r--src/ui/widget/font-variants.cpp1461
-rw-r--r--src/ui/widget/font-variants.h222
-rw-r--r--src/ui/widget/font-variations.cpp179
-rw-r--r--src/ui/widget/font-variations.h126
-rw-r--r--src/ui/widget/frame.cpp80
-rw-r--r--src/ui/widget/frame.h75
-rw-r--r--src/ui/widget/highlight-picker.cpp169
-rw-r--r--src/ui/widget/highlight-picker.h73
-rw-r--r--src/ui/widget/iconrenderer.cpp121
-rw-r--r--src/ui/widget/iconrenderer.h84
-rw-r--r--src/ui/widget/imagetoggler.cpp113
-rw-r--r--src/ui/widget/imagetoggler.h89
-rw-r--r--src/ui/widget/ink-color-wheel.cpp726
-rw-r--r--src/ui/widget/ink-color-wheel.h86
-rw-r--r--src/ui/widget/ink-flow-box.cpp143
-rw-r--r--src/ui/widget/ink-flow-box.h68
-rw-r--r--src/ui/widget/ink-ruler.cpp473
-rw-r--r--src/ui/widget/ink-ruler.h80
-rw-r--r--src/ui/widget/ink-spinscale.cpp285
-rw-r--r--src/ui/widget/ink-spinscale.h96
-rw-r--r--src/ui/widget/insertordericon.cpp118
-rw-r--r--src/ui/widget/insertordericon.h85
-rw-r--r--src/ui/widget/label-tool-item.cpp66
-rw-r--r--src/ui/widget/label-tool-item.h51
-rw-r--r--src/ui/widget/labelled.cpp110
-rw-r--r--src/ui/widget/labelled.h89
-rw-r--r--src/ui/widget/layer-selector.cpp616
-rw-r--r--src/ui/widget/layer-selector.h114
-rw-r--r--src/ui/widget/layertypeicon.cpp113
-rw-r--r--src/ui/widget/layertypeicon.h91
-rw-r--r--src/ui/widget/licensor.cpp156
-rw-r--r--src/ui/widget/licensor.h57
-rw-r--r--src/ui/widget/notebook-page.cpp47
-rw-r--r--src/ui/widget/notebook-page.h59
-rw-r--r--src/ui/widget/object-composite-settings.cpp325
-rw-r--r--src/ui/widget/object-composite-settings.h81
-rw-r--r--src/ui/widget/page-sizer.cpp781
-rw-r--r--src/ui/widget/page-sizer.h307
-rw-r--r--src/ui/widget/pages-skeleton.h154
-rw-r--r--src/ui/widget/panel.cpp176
-rw-r--r--src/ui/widget/panel.h139
-rw-r--r--src/ui/widget/point.cpp180
-rw-r--r--src/ui/widget/point.h196
-rw-r--r--src/ui/widget/preferences-widget.cpp1023
-rw-r--r--src/ui/widget/preferences-widget.h315
-rw-r--r--src/ui/widget/preview.cpp502
-rw-r--r--src/ui/widget/preview.h163
-rw-r--r--src/ui/widget/random.cpp101
-rw-r--r--src/ui/widget/random.h125
-rw-r--r--src/ui/widget/registered-enums.h99
-rw-r--r--src/ui/widget/registered-widget.cpp845
-rw-r--r--src/ui/widget/registered-widget.h458
-rw-r--r--src/ui/widget/registry.cpp53
-rw-r--r--src/ui/widget/registry.h48
-rw-r--r--src/ui/widget/rendering-options.cpp122
-rw-r--r--src/ui/widget/rendering-options.h68
-rw-r--r--src/ui/widget/rotateable.cpp180
-rw-r--r--src/ui/widget/rotateable.h71
-rw-r--r--src/ui/widget/scalar-unit.cpp263
-rw-r--r--src/ui/widget/scalar-unit.h191
-rw-r--r--src/ui/widget/scalar.cpp173
-rw-r--r--src/ui/widget/scalar.h188
-rw-r--r--src/ui/widget/selected-style.cpp1488
-rw-r--r--src/ui/widget/selected-style.h296
-rw-r--r--src/ui/widget/spin-button-tool-item.cpp532
-rw-r--r--src/ui/widget/spin-button-tool-item.h95
-rw-r--r--src/ui/widget/spin-scale.cpp226
-rw-r--r--src/ui/widget/spin-scale.h110
-rw-r--r--src/ui/widget/spin-slider.cpp223
-rw-r--r--src/ui/widget/spin-slider.h106
-rw-r--r--src/ui/widget/spinbutton.cpp137
-rw-r--r--src/ui/widget/spinbutton.h117
-rw-r--r--src/ui/widget/style-subject.cpp189
-rw-r--r--src/ui/widget/style-subject.h133
-rw-r--r--src/ui/widget/style-swatch.cpp373
-rw-r--r--src/ui/widget/style-swatch.h105
-rw-r--r--src/ui/widget/text.cpp60
-rw-r--r--src/ui/widget/text.h81
-rw-r--r--src/ui/widget/tolerance-slider.cpp215
-rw-r--r--src/ui/widget/tolerance-slider.h89
-rw-r--r--src/ui/widget/unit-menu.cpp152
-rw-r--r--src/ui/widget/unit-menu.h146
-rw-r--r--src/ui/widget/unit-tracker.cpp294
-rw-r--r--src/ui/widget/unit-tracker.h87
498 files changed, 155798 insertions, 0 deletions
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 <lauris@kaplinski.com>
+ * Bryce Harrington <brycehar@bryceharrington.org>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * 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 <gtk/gtk.h>
+
+#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<Glib::ustring, GdkPixbuf *>::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<Glib::ustring, GdkPixbuf *>::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 <bryce@bryceharrington.org>
+ * 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 <map>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <glibmm/ustring.h>
+#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<Glib::ustring, GdkPixbuf*> _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 <tweenk@o2.pl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * 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 <giomm/application.h>
+#include <gtkmm/clipboard.h>
+#include "ui/clipboard.h"
+
+// TODO: reduce header bloat if possible
+
+#include "file.h" // for file_import, used in _pasteImage
+#include <glibmm/i18n.h>
+#include <glib/gstdio.h> // 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 <windows.h>
+#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<Glib::ustring> 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<SPItem*> cloned_elements;
+ std::vector<SPCSSAttr*> te_selected_style;
+ std::vector<unsigned> 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<Gtk::Clipboard> _clipboard; ///< Handle to the system wide clipboard - for convenience
+ std::list<Glib::ustring> _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<SPGroup *>(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 <use> rather than <symbol> 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 <b>object(s)</b> 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 <b>object(s)</b> 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 <b>object(s)</b> 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 <svg:path> or <svg:text>.
+ // 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<Glib::ustring> ClipboardManagerImpl::getElementsOfType(SPDesktop *desktop, gchar const* type, gint maxdepth)
+{
+ std::vector<Glib::ustring> 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<Inkscape::XML::Node const *> reprs;
+ if (strcmp(type, "*") == 0){
+ //TODO:Fill vector with all possible elements
+ std::vector<Glib::ustring> 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<Inkscape::XML::Node const *> 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<SPObject*> 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::vector<SPItem*>tr;
+ 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<SPObject const *, Inkscape::XML::Node *> groups;
+
+ sorted_items.insert(sorted_items.end(),cloned_elements.begin(),cloned_elements.end());
+ for(auto sorted_item : sorted_items){
+ SPItem *item = dynamic_cast<SPItem*>(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<SPItem *>(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<SPItem *>(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<SPUse *>(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<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) {
+ _copyGradient(dynamic_cast<SPGradient *>(server));
+ }
+ SPPattern *pattern = dynamic_cast<SPPattern *>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ SPHatch *hatch = dynamic_cast<SPHatch *>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) {
+ _copyGradient(dynamic_cast<SPGradient *>(server));
+ }
+ SPPattern *pattern = dynamic_cast<SPPattern *>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ SPHatch *hatch = dynamic_cast<SPHatch *>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+
+ // For shapes, copy all of the shape's markers
+ SPShape *shape = dynamic_cast<SPShape *>(item);
+ if (shape) {
+ for (auto & i : shape->_marker) {
+ if (i) {
+ _copyNode(i->getRepr(), _doc, _defs);
+ }
+ }
+ }
+
+ // For 3D boxes, copy perspectives
+ {
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ _copyNode(box3d_get_perspective(box)->getRepr(), _doc, _defs);
+ }
+ }
+
+ // Copy text elements
+ {
+ SPText *text = dynamic_cast<SPText *>(item);
+ if (text) {
+ // Copy text paths
+ SPTextPath *textpath = dynamic_cast<SPTextPath *>(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<SPIShapes SPStyle::*>(&SPStyle::shape_inside),
+ reinterpret_cast<SPIShapes SPStyle::*>(&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<SPItem *>(&o);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ }
+
+ // Copy filters
+ if (style->getFilter()) {
+ SPObject *filter = style->getFilter();
+ if (dynamic_cast<SPFilter *>(filter)) {
+ _copyNode(filter->getRepr(), _doc, _defs);
+ }
+ }
+
+ // For lpe items, copy lpe stack if applicable
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(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<SPItem *>(&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<SPItem *>(&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<SPItem *>(&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<Gdk::Pixbuf> 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<Gtk::Clipboard> 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<SPText *>(tc->text);
+ if (textitem) {
+ textitem->rebuildLayout();
+ }
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(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<SPLPEItem *>(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<LivePathEffectObject *>(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<SPItem*> 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<Glib::ustring>::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<Gtk::TargetEntry> 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 <tweenk@o2.pl>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ *
+ * 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 <glibmm/ustring.h>
+
+// 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<Glib::ustring> 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 <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/image.h>
+#include <gtkmm/separatormenuitem.h>
+
+#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<SPObject *>(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<std::vector< SPItem * > >(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<std::vector< SPItem * > >(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. <g id="#g7">, 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<SPObject *>(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<SPItem *> items)
+{
+ _desktop->selection->clear();
+ for(auto & item : items) {
+ if (item->isLocked()) {
+ item->setLocked(false);
+ _desktop->selection->add(item);
+ }
+ }
+}
+
+void ContextMenu::UnHideBelow(std::vector<SPItem *> 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<SPItem*> children;
+
+ sp_item_group_ungroup(static_cast<SPGroup*>(_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<SPItem*> children;
+ sp_item_group_ungroup(static_cast<SPAnchor*>(_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<Gtk::Widget *> children = get_children();
+ for (auto child: children) {
+ if (child->get_name() == "ImageMenuItem") {
+ menuitem = static_cast<Gtk::MenuItem *>(child);
+ content = static_cast<Gtk::Box *>(menuitem->get_child());
+ icon = static_cast<Gtk::Image *>(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 <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 <gtkmm/menu.h>
+
+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<SPItem *> items);
+ void UnHideBelow(std::vector<SPItem *> 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 <jon@joncruz.org>
+ *
+ * Copyright 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "control-manager.h"
+
+#include <algorithm>
+#include <set>
+
+#include <glib-object.h>
+
+#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<ControlFlags>(static_cast<int>(lhs) | static_cast<int>(rhs));
+}
+*/
+
+ControlFlags operator &(ControlFlags lhs, ControlFlags rhs)
+{
+ return static_cast<ControlFlags>(static_cast<int>(lhs) & static_cast<int>(rhs));
+}
+
+ControlFlags operator ^(ControlFlags lhs, ControlFlags rhs)
+{
+ return static_cast<ControlFlags>(static_cast<int>(lhs) ^ static_cast<int>(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<void> &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<void> _sizeChangedSignal;
+ PrefListener _prefHook;
+ int _size; // Size from the grabsize preference
+ int _resize; // Way size should change from grabsize
+ std::vector<SPCanvasItem *> _itemList;
+ std::map<Inkscape::ControlType, std::vector<unsigned int> > _sizeTable;
+ std::map<Inkscape::ControlType, GType> _typeTable;
+ std::map<Inkscape::ControlType, SPCtrlShapeType> _ctrlToShape;
+ std::set<Inkscape::ControlType> _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<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
+ _sizeTable[CTRL_TYPE_ADJ_HANDLE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
+ _sizeTable[CTRL_TYPE_ANCHOR] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17};
+ _sizeTable[CTRL_TYPE_POINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ _sizeTable[CTRL_TYPE_ROTATE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ _sizeTable[CTRL_TYPE_SIZER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ _sizeTable[CTRL_TYPE_SHAPER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17};
+ _sizeTable[CTRL_TYPE_NODE_AUTO] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ _sizeTable[CTRL_TYPE_NODE_CUSP] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
+ _sizeTable[CTRL_TYPE_NODE_SMOOTH] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ _sizeTable[CTRL_TYPE_NODE_SYMETRICAL] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = {1, 1, 1, 1, 1, 1, 1};
+ _sizeTable[CTRL_TYPE_INVISIPOINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
+ }
+ {
+ unsigned int sizes[] = { 5, 7, 9, 11, 13, 15, 17 };
+ _sizeTable[CTRL_TYPE_LPE] = std::vector<unsigned int>(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<void> &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<ControlManagerImpl *>(data)->thingFinalized(wasObj);
+ }
+}
+
+void ControlManagerImpl::thingFinalized(GObject *wasObj)
+{
+ SPCanvasItem *wasItem = reinterpret_cast<SPCanvasItem *>(wasObj);
+ if (wasItem)
+ {
+ std::vector<SPCanvasItem *>::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<void> &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 <jon@joncruz.org>
+ *
+ * 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 <memory>
+#include <sigc++/sigc++.h>
+
+#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<void> &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<ControlManagerImpl> _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 <jon@joncruz.org>
+ *
+ * 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 <tavmjong@free.fr>
+ * Alex Valavanis <valavanisalex@gmail.com>
+ * Patrick Storz <eduard.braun2@gmx.de>
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <iostream>
+
+#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 <gtkosxapplication.h>
+#endif
+
+// ================= Common ====================
+
+std::vector<std::pair<std::pair<unsigned int, Gtk::MenuItem *>, 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<Gtk::CheckMenuItem *>(menu.first.second);
+ Gtk::RadioMenuItem *radio = dynamic_cast<Gtk::RadioMenuItem *>(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<Gtk::Label*>(widget);
+
+ // Or wrapped inside a box which is a child of menuitem (if with icon).
+ if (!label) {
+ Gtk::Box* box = dynamic_cast<Gtk::Box*>(widget);
+ if (box) {
+ std::vector<Gtk::Widget*> children = box->get_children();
+ for (auto child: children) {
+ label = dynamic_cast<Gtk::Label*>(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<ContextMenu*>(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<Gtk::MenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action));
+ menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action));
+ menuitem->signal_deselect().connect(sigc::bind<SPAction*>(sigc::ptr_fun(&deselect_action), action));
+
+ action->signal_set_sensitive.connect(
+ sigc::bind<0>(sigc::ptr_fun(&gtk_widget_set_sensitive), (GtkWidget*)menuitem->gobj()));
+ action->signal_set_name.connect(
+ sigc::bind<Gtk::MenuItem*>(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<SPDesktop*>(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<Gtk::CheckMenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action));
+ menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action));
+ menuitem->signal_deselect().connect(sigc::bind<SPAction*>(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<SPDesktop*, int>(sigc::ptr_fun(&task_activated), dt, i));
+ menuitem->signal_select().connect(
+ sigc::bind<SPDesktop*, Glib::ustring>(sigc::ptr_fun(&select_task), dt, data[i][1]));
+ menuitem->signal_deselect().connect(
+ sigc::bind<SPDesktop*>(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<Gio::File> file = Gio::File::create_for_uri(uri);
+
+ ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::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<Gtk::Menu*>(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<std::pair<unsigned int, Gtk::MenuItem *>, 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<Gtk::RadioMenuItem*>(menuitem);
+ if (radiomenuitem) {
+ radiomenuitem->set_active(true);
+ }
+ }
+ std::pair<std::pair<unsigned int, Gtk::MenuItem *>, 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<SPDesktop*>(view));
+ continue;
+ }
+
+ if (name == "recent-file-list") {
+
+ // Filter recent files to those already opened in Inkscape.
+ Glib::RefPtr<Gtk::RecentFilter> 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<Gtk::RecentChooserMenu*>(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 <bulia@dr.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2003-2014 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/entry.h>
+#include <gtkmm/window.h>
+
+#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<Gtk::Window *>(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<GdkEvent*>(&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 <bulia@dr.com>
+ *
+ * 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 <gtk/gtk.h>
+
+/*
+ * 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 <derekm@hackunix.org>
+ * MenTaLguY <mental@rydia.net>
+ * Kees Cook <kees@outflux.net>
+ * Jon Phillips <jon@rejon.org>
+ * 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 <fstream>
+
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+
+#include <gtkmm/aspectframe.h>
+#include <gtkmm/textview.h>
+
+#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<Glib::ustring> 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 <kees@outflux.net>
+ *
+ * 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 <gtkmm/aboutdialog.h>
+
+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 <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Tim Dwyer <tgdwyer@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2004, 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include <2geom/transforms.h>
+
+#include <utility>
+
+#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<SPItem*> 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<SPItem*> selected(selection->items().begin(), selection->items().end());
+ if (selected.empty()) return;
+
+ //Check 2 or more selected objects
+ std::vector<SPItem*>::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<BBoxSort> ::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<SPItem *> 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<SPItem *> 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<Geom::Point> 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<SPItem*> 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<Geom::Point> 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<SPItem*> 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<SPItem*> 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<SPItem*> 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<Baselines> 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<Geom::Point> 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<Geom::Point> 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 <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * 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 <list>
+#include "ui/widget/panel.h"
+#include "ui/widget/frame.h"
+
+#include <gtkmm/frame.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/label.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/grid.h>
+
+#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<Action *> _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 <gtkmm/box.h>
+
+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 <doctormo@gmail.com>
+ *
+ * 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 <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+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<Inkscape::MessageStack>();
+ _message_context = std::unique_ptr<Inkscape::MessageContext>(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<Gtk::TextBuffer> 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<AttrDialog *>(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<Gtk::Entry *>(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<AttrDialog *>(data);
+ attrdialog->_popover->show_all();
+
+ return FALSE;
+}
+
+gboolean sp_close_entry(gpointer data)
+{
+ Gtk::CellEditable *cell = reinterpret_cast<Gtk::CellEditable *>(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<Gtk::Entry *>(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<Gtk::TextBuffer> 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<Gtk::TextBuffer> 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, _("<b>Click</b> attribute to edit."));
+ } else {
+ const gchar *name = g_quark_to_string(attr);
+ _message_context->setF(
+ Inkscape::NORMAL_MESSAGE,
+ _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> 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<AttrDialog *>(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<Gtk::TextBuffer> 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 <doctormo@gmail.com>
+ *
+ * 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 <gtkmm/dialog.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <ui/widget/panel.h>
+
+#define ATTR_DIALOG(obj) (dynamic_cast<Inkscape::UI::Dialog::AttrDialog*>((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<Glib::ustring> _attributeName;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValue;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValueRender;
+ };
+ AttrColumns _attrColumns;
+
+ // TreeView
+ Gtk::TreeView _treeView;
+ Glib::RefPtr<Gtk::ListStore> _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<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _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 <broberg@kth.se>
+ *
+ * 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 <gtkmm/button.h>
+#include <gtkmm/box.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Dialog;
+
+namespace Behavior {
+
+class Behavior;
+
+typedef Behavior *(*BehaviorFactory)(Dialog &dialog);
+
+template <typename T>
+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<void> signal_show() =0;
+ virtual Glib::SignalProxy0<void> signal_hide() =0;
+ virtual Glib::SignalProxy1<bool, GdkEventAny *> 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 <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+
+#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 <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+
+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 <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * 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 <glibmm/i18n.h>
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/radiobutton.h>
+
+#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, _("<b>P1</b>: simple translation")},
+ {TILE_P2, _("<b>P2</b>: 180&#176; rotation")},
+ {TILE_PM, _("<b>PM</b>: reflection")},
+ // TRANSLATORS: "glide reflection" is a reflection and a translation combined.
+ // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html
+ {TILE_PG, _("<b>PG</b>: glide reflection")},
+ {TILE_CM, _("<b>CM</b>: reflection + glide reflection")},
+ {TILE_PMM, _("<b>PMM</b>: reflection + reflection")},
+ {TILE_PMG, _("<b>PMG</b>: reflection + 180&#176; rotation")},
+ {TILE_PGG, _("<b>PGG</b>: glide reflection + 180&#176; rotation")},
+ {TILE_CMM, _("<b>CMM</b>: reflection + reflection + 180&#176; rotation")},
+ {TILE_P4, _("<b>P4</b>: 90&#176; rotation")},
+ {TILE_P4M, _("<b>P4M</b>: 90&#176; rotation + 45&#176; reflection")},
+ {TILE_P4G, _("<b>P4G</b>: 90&#176; rotation + 90&#176; reflection")},
+ {TILE_P3, _("<b>P3</b>: 120&#176; rotation")},
+ {TILE_P31M, _("<b>P31M</b>: reflection + 120&#176; rotation, dense")},
+ {TILE_P3M1, _("<b>P3M1</b>: reflection + 120&#176; rotation, sparse")},
+ {TILE_P6, _("<b>P6</b>: 60&#176; rotation")},
+ {TILE_P6M, _("<b>P6M</b>: reflection + 60&#176; 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), _("<b>Shift X:</b>"));
+ 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), _("<b>Shift Y:</b>"));
+ 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), _("<b>Exponent:</b>"));
+ 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), _("<small>Alternate:</small>"));
+ 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), _("<small>Cumulate:</small>"));
+ 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), _("<small>Exclude tile:</small>"));
+ 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), _("<b>Scale X:</b>"));
+ 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), _("<b>Scale Y:</b>"));
+ 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), _("<b>Exponent:</b>"));
+ 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), _("<b>Base:</b>"));
+ 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), _("<small>Alternate:</small>"));
+ 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), _("<small>Cumulate:</small>"));
+ 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), _("<b>Angle:</b>"));
+ 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, "&#176;");
+ 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, "&#176;");
+ 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), _("<small>Alternate:</small>"));
+ 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), _("<small>Cumulate:</small>"));
+ 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), _("<b>Blur:</b>"));
+ 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), _("<small>Alternate:</small>"));
+ 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), _("<b>Opacity:</b>"));
+ 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), _("<small>Alternate:</small>"));
+ 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<GtkWidget*>(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), _("<b>H:</b>"));
+ 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), _("<b>S:</b>"));
+ 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), _("<b>L:</b>"));
+ 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), _("<small>Alternate:</small>"));
+ 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("&#215;");
+ _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("&#215;");
+ _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(_(" <b>_Create</b> "));
+ 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), _("<small>Nothing selected.</small>"));
+ return;
+ }
+
+ if (boost::distance(selection->items()) > 1) {
+ gtk_widget_set_sensitive (_buttons_on_tiles, FALSE);
+ gtk_label_set_markup (GTK_LABEL(_status), _("<small>More than one object selected.</small>"));
+ return;
+ }
+
+ guint n = number_of_clones(selection->singleItem());
+ if (n > 0) {
+ gtk_widget_set_sensitive (_buttons_on_tiles, TRUE);
+ gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), 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), _("<small>Object has no tiled clones.</small>"));
+ }
+}
+
+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<SPUse *>(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<SPItem *>(&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 <b>one object</b> whose tiled clones to unclump."));
+ return;
+ }
+
+ auto obj = selection->singleItem();
+ auto parent = obj->parent;
+
+ std::vector<SPItem*> 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 <b>one object</b> whose tiled clones to remove."));
+ return;
+ }
+
+ SPObject *obj = selection->singleItem();
+ SPObject *parent = obj->parent;
+
+// remove old tiling
+ std::vector<SPObject *> 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 <b>object</b> 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, <b>group</b> them and <b>clone the group</b>."));
+ return;
+ }
+
+ // set "busy" cursor
+ desktop->setWaitingCursor();
+
+ // set statusbar text
+ gtk_label_set_markup (GTK_LABEL(_status), _("<small>Creating tiled clones...</small>"));
+ 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<SPItem *>(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], &notused ); // 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<SPItem *>(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<Gtk::Adjustment> &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<Gtk::Adjustment> &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<Gtk::Widget*> 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), _("<small>Per row:</small>"));
+ 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), _("<small>Per column:</small>"));
+ 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), _("<small>Randomize:</small>"));
+ 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 <buliabyak@users.sf.net>
+ *
+ * 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<Gtk::Adjustment> &adj, Glib::ustring const &pref);
+ void xy_changed(Glib::RefPtr<Gtk::Adjustment> &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<Gtk::Adjustment> fill_width;
+ Glib::RefPtr<Gtk::Adjustment> 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 <cerrno>
+
+#include <gtkmm/label.h>
+#include <glibmm/i18n.h>
+
+#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<std::string> mimeStrings;
+static std::map<std::string, guint> mimeToInt;
+
+
+#if ENABLE_MAGIC_COLORS
+// TODO remove this soon:
+extern std::vector<SwatchPage*> 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<SPItem*>(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: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> 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<Gdk::DragContext> &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<Gdk::Pixbuf> 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<UI::Widget::Preview *>(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<UI::Widget::LinkType>(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<UI::Widget::Preview *>(widget);
+ auto label = dynamic_cast<Gtk::Label *>(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<Gdk::DragContext>& /*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<ColorItem*>(data);
+ if ( item ) {
+ item->_updatePreviews();
+ }
+}
+
+void ColorItem::_updatePreviews()
+{
+ for (auto widget : _previews) {
+ auto preview = dynamic_cast<UI::Widget::Preview *>(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<SwatchPage*>::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) {
+ SwatchPage* curr = *it2;
+ index = 0;
+ for ( boost::ptr_vector<ColorItem>::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<UI::Widget::LinkType>( (_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<Gtk::TargetEntry> 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<ColorItem>::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 <boost/ptr_container/ptr_vector.hpp>
+
+#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<ColorItem> _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<Gdk::DragContext> &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<Gdk::DragContext> &dc);
+ void handleClick();
+ void handleSecondaryClick(gint arg1);
+ bool handleEnterNotify(GdkEventCrossing* event);
+ bool handleLeaveNotify(GdkEventCrossing* event);
+
+ std::vector<Gtk::Widget*> _previews;
+
+ bool _isFill;
+ bool _isStroke;
+ bool _isLive;
+ bool _linkIsTone;
+ int _linkPercent;
+ int _linkGray;
+ ColorItem* _linkSrc;
+ SPGradient* _grad;
+ cairo_pattern_t *_pattern;
+ std::vector<ColorItem*> _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 <gtkmm/box.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <glibmm/i18n.h>
+
+#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<Gtk::TextBuffer> 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<Gtk::TextBuffer> 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<DebugDialogImpl *>(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<void, SPDesktop*> & 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 <cstddef>
+#include <sigc++/connection.h>
+
+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<void, SPDesktop*> & 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<void, SPDesktop*> 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 <bryce@bryceharrington.org>
+ * Jon Phillips <jon@rejon.org>
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * 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 <typename T, typename B>
+inline Dialog *create() { return PanelDialog<B>::template create<T>(); }
+
+}
+
+/**
+ * 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<InkscapePreferences, FloatingBehavior>);
+
+ if (dialogs_type == FLOATING) {
+ registerFactory("AlignAndDistribute", &create<AlignAndDistribute, FloatingBehavior>);
+ registerFactory("DocumentMetadata", &create<DocumentMetadata, FloatingBehavior>);
+ registerFactory("DocumentProperties", &create<DocumentProperties, FloatingBehavior>);
+ registerFactory("ExtensionEditor", &create<ExtensionEditor, FloatingBehavior>);
+ registerFactory("FillAndStroke", &create<FillAndStroke, FloatingBehavior>);
+ registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, FloatingBehavior>);
+ registerFactory("FilterEditorDialog", &create<FilterEditorDialog, FloatingBehavior>);
+ registerFactory("Find", &create<Find, FloatingBehavior>);
+ registerFactory("Glyphs", &create<GlyphsPanel, FloatingBehavior>);
+ registerFactory("IconPreviewPanel", &create<IconPreviewPanel, FloatingBehavior>);
+ registerFactory("LayersPanel", &create<LayersPanel, FloatingBehavior>);
+ registerFactory("ObjectsPanel", &create<ObjectsPanel, FloatingBehavior>);
+ registerFactory("TagsPanel", &create<TagsPanel, FloatingBehavior>);
+ registerFactory("LivePathEffect", &create<LivePathEffectEditor, FloatingBehavior>);
+ registerFactory("Memory", &create<Memory, FloatingBehavior>);
+ registerFactory("Messages", &create<Messages, FloatingBehavior>);
+ registerFactory("ObjectAttributes", &create<ObjectAttributes, FloatingBehavior>);
+ registerFactory("ObjectProperties", &create<ObjectProperties, FloatingBehavior>);
+// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, FloatingBehavior>);
+ registerFactory("SvgFontsDialog", &create<SvgFontsDialog, FloatingBehavior>);
+ registerFactory("Swatches", &create<SwatchesPanel, FloatingBehavior>);
+ registerFactory("TileDialog", &create<ArrangeDialog, FloatingBehavior>);
+ registerFactory("Symbols", &create<SymbolsDialog, FloatingBehavior>);
+ registerFactory("PaintServers", &create<PaintServersDialog, FloatingBehavior>);
+ registerFactory("StyleDialog", &create<StyleDialog, FloatingBehavior>);
+ registerFactory("Trace", &create<TraceDialog, FloatingBehavior>);
+
+ registerFactory("Transformation", &create<Transformation, FloatingBehavior>);
+ registerFactory("UndoHistory", &create<UndoHistory, FloatingBehavior>);
+ registerFactory("InputDevices", &create<InputDialog, FloatingBehavior>);
+ registerFactory("TextFont", &create<TextEdit, FloatingBehavior>);
+
+#if HAVE_ASPELL
+ registerFactory("SpellCheck", &create<SpellCheck, FloatingBehavior>);
+#endif
+
+ registerFactory("Export", &create<Export, FloatingBehavior>);
+ registerFactory("CloneTiler", &create<CloneTiler, FloatingBehavior>);
+ registerFactory("XmlTree", &create<XmlTree, FloatingBehavior>);
+ registerFactory("Selectors", &create<SelectorsDialog, FloatingBehavior>);
+
+ } else {
+
+ registerFactory("AlignAndDistribute", &create<AlignAndDistribute, DockBehavior>);
+ registerFactory("DocumentMetadata", &create<DocumentMetadata, DockBehavior>);
+ registerFactory("DocumentProperties", &create<DocumentProperties, DockBehavior>);
+ registerFactory("ExtensionEditor", &create<ExtensionEditor, DockBehavior>);
+ registerFactory("FillAndStroke", &create<FillAndStroke, DockBehavior>);
+ registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, DockBehavior>);
+ registerFactory("FilterEditorDialog", &create<FilterEditorDialog, DockBehavior>);
+ registerFactory("Find", &create<Find, DockBehavior>);
+ registerFactory("Glyphs", &create<GlyphsPanel, DockBehavior>);
+ registerFactory("IconPreviewPanel", &create<IconPreviewPanel, DockBehavior>);
+ registerFactory("LayersPanel", &create<LayersPanel, DockBehavior>);
+ registerFactory("ObjectsPanel", &create<ObjectsPanel, DockBehavior>);
+ registerFactory("TagsPanel", &create<TagsPanel, DockBehavior>);
+ registerFactory("LivePathEffect", &create<LivePathEffectEditor, DockBehavior>);
+ registerFactory("Memory", &create<Memory, DockBehavior>);
+ registerFactory("Messages", &create<Messages, DockBehavior>);
+ registerFactory("ObjectAttributes", &create<ObjectAttributes, DockBehavior>);
+ registerFactory("ObjectProperties", &create<ObjectProperties, DockBehavior>);
+// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, DockBehavior>);
+ registerFactory("SvgFontsDialog", &create<SvgFontsDialog, DockBehavior>);
+ registerFactory("Swatches", &create<SwatchesPanel, DockBehavior>);
+ registerFactory("TileDialog", &create<ArrangeDialog, DockBehavior>);
+ registerFactory("Symbols", &create<SymbolsDialog, DockBehavior>);
+ registerFactory("PaintServers", &create<PaintServersDialog, DockBehavior>);
+ registerFactory("Trace", &create<TraceDialog, DockBehavior>);
+
+ registerFactory("Transformation", &create<Transformation, DockBehavior>);
+ registerFactory("UndoHistory", &create<UndoHistory, DockBehavior>);
+ registerFactory("InputDevices", &create<InputDialog, DockBehavior>);
+ registerFactory("TextFont", &create<TextEdit, DockBehavior>);
+
+#if HAVE_ASPELL
+ registerFactory("SpellCheck", &create<SpellCheck, DockBehavior>);
+#endif
+
+ registerFactory("Export", &create<Export, DockBehavior>);
+ registerFactory("CloneTiler", &create<CloneTiler, DockBehavior>);
+ registerFactory("XmlTree", &create<XmlTree, DockBehavior>);
+ registerFactory("Selectors", &create<SelectorsDialog, DockBehavior>);
+ }
+}
+
+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 <bryce@bryceharrington.org>
+ * Jon Phillips <jon@rejon.org>
+ *
+ * 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 <map>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogManager {
+public:
+ typedef Dialog *(*DialogFactory)();
+
+ DialogManager();
+ virtual ~DialogManager();
+
+ static DialogManager &getInstance();
+
+ // sigc::signal<void> show_dialogs;
+ // sigc::signal<void> show_f12;
+ // sigc::signal<void> hide_dialogs;
+ // sigc::signal<void> hide_f12;
+ // sigc::signal<void> transientize;
+
+ /* generic dialog management start */
+ typedef std::map<GQuark, DialogFactory> FactoryMap;
+ typedef std::map<GQuark, Dialog*> 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 <bryce@bryceharrington.org>
+ * buliabyak@gmail.com
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2004--2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-manager.h"
+#include <gtkmm/dialog.h>
+
+#include <gdk/gdkkeysyms.h>
+
+#include <utility>
+
+#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<Dialog *>(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<Gtk::Widget *>(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<void> Dialog::signal_show() { return _behavior->signal_show(); }
+Glib::SignalProxy0<void> 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<Gtk::Window *>(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<Gtk::Window *>(canvas->get_toplevel());
+ if (toplevel_window) {
+ Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(widg);
+ if (!current_window) {
+ current_window = dynamic_cast<Gtk::Window *>(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 <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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<void> signal_show();
+ virtual Glib::SignalProxy0<void> 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 <broberg@kth.se>
+ *
+ * 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<Widget::DockItem::State>(
+ Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/state",
+ UI::Widget::DockItem::DOCKED_STATE)),
+ static_cast<GdlDockPlacement>(
+ 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<void>
+DockBehavior::signal_show() { return _dock_item.signal_show(); }
+
+Glib::SignalProxy0<void>
+DockBehavior::signal_hide() { return _dock_item.signal_hide(); }
+
+Glib::SignalProxy1<bool, GdkEventAny *>
+DockBehavior::signal_delete_event() { return _dock_item.signal_delete_event(); }
+
+Glib::SignalProxy0<void>
+DockBehavior::signal_drag_begin() { return _dock_item.signal_drag_begin(); }
+
+Glib::SignalProxy1<void, bool>
+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 <broberg@kth.se>
+ *
+ * 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<void> signal_show() override;
+ Glib::SignalProxy0<void> signal_hide() override;
+ Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() override;
+ Glib::SignalProxy0<void> signal_drag_begin();
+ Glib::SignalProxy1<void, bool> 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 <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (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 (_("<b>Dublin Core Entities</b>"));
+ 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 (_("<b>License</b>"));
+ 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<DocumentMetadata *>(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 <ralf@ark.in-berlin.de>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * 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 <list>
+#include <cstddef>
+#include "ui/widget/panel.h"
+#include <gtkmm/notebook.h>
+#include <gtkmm/grid.h>
+
+#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<UI::Widget::EntityEntry*> 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 <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Diederik van Lierop <mail@diedenrezi.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl>
+ * 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 <vector>
+#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<Inkscape::UI::Widget::PageSizer*>(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<Gtk::Label&>(*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 (_("<b>General</b>"));
+
+ Gtk::Label *label_for = Gtk::manage (new Gtk::Label);
+ label_for->set_markup (_("<b>Page Size</b>"));
+
+ Gtk::Label* label_bkg = Gtk::manage (new Gtk::Label);
+ label_bkg->set_markup (_("<b>Background</b>"));
+
+ Gtk::Label* label_bdr = Gtk::manage (new Gtk::Label);
+ label_bdr->set_markup (_("<b>Border</b>"));
+
+ Gtk::Label* label_dsp = Gtk::manage (new Gtk::Label);
+ label_dsp->set_markup (_("<b>Display</b>"));
+
+ _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<Gtk::Widget*> _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 (_("<b>Guides</b>"));
+
+ _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 (_("<b>Snap to objects</b>"));
+ Gtk::Label *label_gr = Gtk::manage (new Gtk::Label);
+ label_gr->set_markup (_("<b>Snap to grids</b>"));
+ Gtk::Label *label_gu = Gtk::manage (new Gtk::Label);
+ label_gu->set_markup (_("<b>Snap to guides</b>"));
+ Gtk::Label *label_m = Gtk::manage (new Gtk::Label);
+ label_m->set_markup (_("<b>Miscellaneous</b>"));
+
+ 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] = "<separator>";
+ row[_AvailableProfilesListColumns.nameColumn] = "<separator>";
+ 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 <color-profile> 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<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" );
+ for (auto obj : current) {
+ Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(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<const Inkscape::ColorProfile &>(*a);
+ const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*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 <typename From, typename To>
+struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } };
+
+void DocumentProperties::populate_linked_profiles_box()
+{
+ _LinkedProfilesListStore->clear();
+ std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" );
+ if (! current.empty()) {
+ _emb_profiles_observer.set((*(current.begin()))->parent);
+ }
+
+ std::set<Inkscape::ColorProfile *, Inkscape::ColorProfile::pointerComparator> _current;
+ std::transform(current.begin(),
+ current.end(),
+ std::inserter(_current, _current.begin()),
+ static_caster<SPObject, Inkscape::ColorProfile>());
+
+ 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<GdkEvent *>(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<GdkEvent *>(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<GdkEvent *>(event));
+ }
+}
+
+void DocumentProperties::cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> 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<void> 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<void> 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<Gtk::TreeSelection> 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<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" );
+ for (auto obj : current) {
+ Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(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<Gtk::TreeModel>& 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 (_("<b>Linked Color Profiles:</b>"));
+ Gtk::Label *label_avail = Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_avail->set_markup (_("<b>Available Color Profiles:</b>"));
+
+ _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<SPObject *> 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 (_("<b>External script files:</b>"));
+
+ _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 (_("<b>Embedded script files:</b>"));
+
+ _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 (_("<b>Content:</b>"));
+
+ 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<SPObject *> 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 (_("<b>Dublin Core Entities</b>"));
+ 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 (_("<b>License</b>"));
+ 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<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" );
+ for (auto obj : current) {
+ if (obj) {
+ SPScript* script = dynamic_cast<SPScript *>(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<Gtk::TreeSelection> sel = _ExternalScriptsList.get_selection();
+ if (sel) {
+ _external_remove_btn.set_sensitive(sel->count_selected_rows () > 0);
+ }
+}
+
+void DocumentProperties::onEmbeddedScriptSelectRow()
+{
+ Glib::RefPtr<Gtk::TreeSelection> 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<SPObject *> 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<SPObject *> 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<SPObject*> 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<SPObject *> 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<SPScript *>(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(_("<b>Creation</b>"));
+ _grids_label_def.set_markup(_("<b>Defined grids</b>"));
+ _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<DocumentProperties *>(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<DocumentProperties *>(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<DocumentProperties *>(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 <ralf@ark.in-berlin.de>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl>
+ * 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 <cstddef>
+#include <sigc++/sigc++.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/textview.h>
+
+#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<UI::Widget::EntityEntry*> 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<void> 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<void> rem);
+ void embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> 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<Glib::ustring> fileColumn;
+ Gtk::TreeModelColumn<Glib::ustring> nameColumn;
+ Gtk::TreeModelColumn<bool> separatorColumn;
+ };
+ AvailableProfilesColumns _AvailableProfilesListColumns;
+ Glib::RefPtr<Gtk::ListStore> _AvailableProfilesListStore;
+ Gtk::ComboBox _AvailableProfilesList;
+ bool _AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter);
+ class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ LinkedProfilesColumns()
+ { add(nameColumn); add(previewColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> nameColumn;
+ Gtk::TreeModelColumn<Glib::ustring> previewColumn;
+ };
+ LinkedProfilesColumns _LinkedProfilesListColumns;
+ Glib::RefPtr<Gtk::ListStore> _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<Glib::ustring> filenameColumn;
+ };
+ ExternalScriptsColumns _ExternalScriptsListColumns;
+ class EmbeddedScriptsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ EmbeddedScriptsColumns()
+ { add(idColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> idColumn;
+ };
+ EmbeddedScriptsColumns _EmbeddedScriptsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _ExternalScriptsListStore;
+ Glib::RefPtr<Gtk::ListStore> _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 <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Peter Bostrom
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 <png.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/spinbutton.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+
+#include <gdl/gdl-dock-item.h>
+
+#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 <windows.h>
+#include <commdlg.h>
+#include <gdk/gdkwin32.h>
+#include <glibmm/fileutils.h>
+#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(_("<b>Export area</b>"), 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(_("<b>Image size</b>"), 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(_("<b>_Filename</b>"), 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::Output *>(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<Gtk::Adjustment> 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<Gtk::Dialog*>(dlg);
+
+ Export *self = reinterpret_cast<Export *>(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<double>(total);
+
+ value = completed + (value / static_cast<double>(total));
+ }
+
+ Gtk::ProgressBar *prg = reinterpret_cast<Gtk::ProgressBar *>(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 <b>%s</b>..."), safeFile), desktop);
+ MessageCleaner msgFlashCleanup(desktop->messageStack()->flashF(Inkscape::IMMEDIATE_MESSAGE,
+ _("Exporting file <b>%s</b>..."), safeFile), desktop);
+ std::vector<SPItem*> x;
+ std::vector<SPItem*> 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 <b>%s</b>."), 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 <b>%d</b> files from <b>%d</b> 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<SPItem*> x;
+ std::vector<SPItem*> 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 <b>%s</b>."), 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<wchar_t*>(utf16_path_string), _MAX_PATH);
+ g_free(utf16_path_string);
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Glib::RefPtr<const Gdk::Window> 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<Gtk::Adjustment>& 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<Gtk::Adjustment>& 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<Gtk::Adjustment>& 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<Gtk::Adjustment>& 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<Gtk::Adjustment>& 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<Gtk::Adjustment>& 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 <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * 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 <gtkmm/progressbar.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/comboboxtext.h>
+
+#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<Gtk::Adjustment>& adj, double val);
+ void setValuePx(Glib::RefPtr<Gtk::Adjustment>& adj, double val);
+ float getValue(Glib::RefPtr<Gtk::Adjustment>& adj);
+ float getValuePx(Glib::RefPtr<Gtk::Adjustment>& 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<Gtk::Adjustment> 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<Gtk::Adjustment>& adj);
+
+ /**
+ * Area Y value changed callback
+ */
+ void onAreaY0Change() {
+ areaYChange(y0_adj);
+ } ;
+ void onAreaY1Change() {
+ areaYChange(y1_adj);
+ } ;
+ void areaYChange(Glib::RefPtr<Gtk::Adjustment>& 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<Gtk::Adjustment> x0_adj;
+ Glib::RefPtr<Gtk::Adjustment> x1_adj;
+ Glib::RefPtr<Gtk::Adjustment> y0_adj;
+ Glib::RefPtr<Gtk::Adjustment> y1_adj;
+ Glib::RefPtr<Gtk::Adjustment> width_adj;
+ Glib::RefPtr<Gtk::Adjustment> height_adj;
+
+ /* Bitmap size widgets */
+ Glib::RefPtr<Gtk::Adjustment> bmwidth_adj;
+ Glib::RefPtr<Gtk::Adjustment> bmheight_adj;
+ Glib::RefPtr<Gtk::Adjustment> xdpi_adj;
+ Glib::RefPtr<Gtk::Adjustment> 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<Gtk::Adjustment> 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 <bryce@bryceharrington.org>
+ * Ted Gould <ted@gould.cx>
+ *
+ * Copyright (C) 2004-2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "extension-editor.h"
+#include <glibmm/i18n.h>
+
+#include <gtkmm/frame.h>
+#include <gtkmm/notebook.h>
+
+#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<Gtk::TreeSelection> 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<Gtk::TreeSelection> 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<ExtensionEditor *>(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 <bryce@bryceharrington.org>
+ * Ted Gould <ted@gould.cx>
+ *
+ * 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 <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/scrolledwindow.h>
+
+#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<Gtk::TreeStore> _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<Glib::ustring> _col_name;
+ /** \brief ID of the extension */
+ Gtk::TreeModelColumn<Glib::ustring> _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 <gtkmm/scrolledwindow.h>
+
+#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<ExtensionsPanel*>(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 <gtkmm/textview.h>
+
+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 <johan@shouraizou.nl>
+ * 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 <glibmm/convert.h>
+
+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<Glib::ustring::value_type>( g_ascii_tolower( static_cast<gchar>(0x07f & ch) ) ) != ext[extpos] )
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool isValidImageFile(const Glib::ustring &fileName)
+{
+ std::vector<Gdk::PixbufFormat>formats = Gdk::Pixbuf::get_formats();
+ for (auto format : formats)
+ {
+ std::vector<Glib::ustring>extensions = 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 <rwjj@earthlink.net>
+ * Joel Holdsworth
+ * Inkscape Guys
+ *
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * 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 <vector>
+#include <set>
+
+#include "extension/system.h"
+
+#include <glibmm/ustring.h>
+
+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<Glib::ustring> 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<Glib::ustring, Inkscape::Extension::Output*> 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 <johan@shouraizou.nl>
+ * 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 <iostream>
+
+#include <glibmm/convert.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/regex.h>
+#include <gtkmm/expander.h>
+
+#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<Gtk::Entry *> &result)
+{
+ if (!parent) {
+ return;
+ }
+ std::vector<Gtk::Widget *> children = parent->get_children();
+ for (auto child : children) {
+ GtkWidget *wid = child->gobj();
+ if (GTK_IS_ENTRY(wid))
+ result.push_back(dynamic_cast<Gtk::Entry *>(child));
+ else if (GTK_IS_CONTAINER(wid))
+ findEntryWidgets(dynamic_cast<Gtk::Container *>(child), result);
+ }
+}
+
+void findExpanderWidgets(Gtk::Container *parent, std::vector<Gtk::Expander *> &result)
+{
+ if (!parent)
+ return;
+ std::vector<Gtk::Widget *> children = parent->get_children();
+ for (auto child : children) {
+ GtkWidget *wid = child->gobj();
+ if (GTK_IS_EXPANDER(wid))
+ result.push_back(dynamic_cast<Gtk::Expander *>(child));
+ else if (GTK_IS_CONTAINER(wid))
+ findExpanderWidgets(dynamic_cast<Gtk::Container *>(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<Glib::ustring> FileOpenDialogImplGtk::getFilenames()
+{
+ auto result_tmp = get_filenames();
+
+ // Copy filenames to a vector of type Glib::ustring
+ std::vector<Glib::ustring> 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<Gtk::Entry *> 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<Gtk::Expander *> 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<Glib::ustring> 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<Inkscape::Extension::Output *>(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<Glib::ustring, Inkscape::Extension::Output*>(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<Inkscape::Extension::Output *>(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<Inkscape::Extension::Output *>(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 <johan@shouraizou.nl>
+ * 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 <gtkmm/filechooserdialog.h>
+#include <glib/gstdio.h>
+
+#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<Gtk::Entry *> &result);
+
+void
+findExpanderWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Expander *> &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<Glib::ustring> 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<Glib::ustring, Inkscape::Extension::Extension *> 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<FileType> 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 <cairomm/win32_surface.h>
+#include <gdk/gdkwin32.h>
+#include <gdkmm/general.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <list>
+#include <vector>
+
+//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<const Gdk::Window> 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> 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<Filter>::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> 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<Filter>::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<FileOpenDialogImplWin32*>
+ (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<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileOpenDialogImplWin32*>(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<LONG_PTR>(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<UINT_PTR>(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<OFNOTIFY*>(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<FileOpenDialogImplWin32*>
+ (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<FileOpenDialogImplWin32*>
+ (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<HFONT>(SendMessage(GetParent(hwnd),
+ WM_GETFONT, 0, 0));
+ HFONT hOldFont = static_cast<HFONT>(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<HBRUSH>(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<HBRUSH>(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<HBRUSH>(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<Win32Surface> surface = Win32Surface::create(hMemDC);
+ Cairo::RefPtr<Context> 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<LinearGradient> 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<LinearGradient> 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<LinearGradient> 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<LinearGradient> 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<RadialGradient> 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<RadialGradient> 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<RadialGradient> 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<RadialGradient> 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::vector<Glib::ustring>FileOpenDialogImplWin32::getFilenames()
+{
+ std::vector<Glib::ustring> 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> 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, Inkscape::Extension::Output*>(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<Filter>::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> 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, Inkscape::Extension::Output*>(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<Filter>::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<FileSaveDialogImplWin32*>
+ (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<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileSaveDialogImplWin32*>(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 <glibmm.h>
+
+#ifdef _WIN32
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "inkgc/gc-core.h"
+
+#include <windows.h>
+
+
+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<Glib::ustring> 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<Gdk::Pixbuf> _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 <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * 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 <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * 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 <gtkmm/notebook.h>
+#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 <string>
+
+#include <gtkmm.h>
+
+#include <gdkmm/display.h>
+#include <gdkmm/seat.h>
+
+#include <glibmm/convert.h>
+#include <glibmm/error.h>
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/stringutils.h>
+
+#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<Gtk::ListStore> fs = Glib::RefPtr<Gtk::ListStore>::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 <gtkmm/notebook.h>
+#include <gtkmm/sizegroup.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/liststore.h>
+
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+
+#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<Gtk::Builder> builder;
+ Glib::RefPtr<Glib::Object> 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 <nicholasbishop@gmail.org>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * insaner
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/imagemenuitem.h>
+
+#include <gdkmm/display.h>
+#include <gdkmm/general.h>
+#include <gdkmm/seat.h>
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/sizegroup.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/main.h>
+#include <glibmm/convert.h>
+
+#include <utility>
+
+#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>(T default_value, const Util::EnumDataConverter<T>& c, const SPAttributeEnum a = SP_ATTR_INVALID, char* tip_text = nullptr)
+ {
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ combo = new ComboBoxEnum<T>(default_value, c, a, false);
+ add(*combo);
+ show_all();
+ }
+
+ ~ComboWithTooltip() override
+ {
+ delete combo;
+ }
+
+ ComboBoxEnum<T>* get_attrwidget()
+ {
+ return combo;
+ }
+private:
+ ComboBoxEnum<T>* 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<SPAttributeEnum> attrs, std::vector<double> default_values, std::vector<char*> 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<SpinButtonAttr*>& get_spinbuttons()
+ {
+ return _spins;
+ }
+private:
+ std::vector<SpinButtonAttr*> _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<double> get_values() const
+ {
+ std::vector<double> 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<double>& 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<Gtk::TreeModelColumn<double> > cols;
+ };
+
+ void update(SPObject* o, const int rows, const int cols)
+ {
+ if(_locked)
+ return;
+
+ _model->clear();
+
+ _tree.remove_all_columns();
+
+ std::vector<gdouble>* 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<Gtk::CellRendererText*>(
+ _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<Gtk::ListStore> _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<const AttrWidget*>(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<double> _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<void, const AttrWidget*> 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<SPAttributeEnum> attrs;
+ attrs.push_back(attr1);
+ attrs.push_back(attr2);
+
+ std::vector<double> default_values;
+ default_values.push_back(def1);
+ default_values.push_back(def2);
+
+ std::vector<char*> 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<SPAttributeEnum> attrs;
+ attrs.push_back(attr1);
+ attrs.push_back(attr2);
+ attrs.push_back(attr3);
+
+ std::vector<double> default_values;
+ default_values.push_back(def1);
+ default_values.push_back(def2);
+ default_values.push_back(def3);
+
+ std::vector<char*> 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<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttributeEnum attr,
+ const Glib::ustring& label,
+ const Util::EnumDataConverter<T>& conv, char* tip_text = nullptr)
+ {
+ ComboWithTooltip<T>* combo = new ComboWithTooltip<T>(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<Gtk::SizeGroup> _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<Gtk::VBox*> _groups;
+ FilterEffectsDialog& _dialog;
+ SetAttrSlot _set_attr_slot;
+ std::vector<std::vector< AttrWidget*> > _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 <funcNode>
+ 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<FilterComponentTransferType> _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<LightSource> _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<void> dup,
+ sigc::slot<void> 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<SPObject*> 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<Gdk::DragContext>& /*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<SPObject *> 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<GdkEvent *>(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<SPItem*> x,y;
+ std::vector<SPItem*> all = get_all_items(x, _desktop->currentRoot(), _desktop, false, false, true, y);
+ for(std::vector<SPItem*>::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<void*> 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<PrimitiveList&>(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<Pango::Context> 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<void>& 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<void> dup,
+ sigc::slot<void> 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<Cairo::Context> & 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<Gdk::Point> 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<Cairo::Context>& 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<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& 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<Gdk::Point>& 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<Gdk::Point> 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<int>(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<GdkEvent *>(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<Gdk::DragContext>& /*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<Gtk::ScrolledWindow*>(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<Gtk::ScrolledWindow*>(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<FilterColorMatrixType>* 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 <b>feBlend</b> 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 <b>feColorMatrix</b> 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 <b>feComponentTransfer</b> 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 <b>feComposite</b> 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 <b>feConvolveMatrix</b> 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 <b>feDiffuseLighting</b> 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 <b>feDisplacementMap</b> 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 <b>feFlood</b> 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 <b>feGaussianBlur</b> 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 <b>feImage</b> 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 <b>feMerge</b> 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 <b>feMorphology</b> 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 <b>feOffset</b> 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 <b>feDiffuseLighting</b> and <b>feSpecularLighting</b> 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 <b>feTile</b> 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 <b>feTurbulence</b> 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<Gtk::Widget*> 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<Gtk::Widget*> 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<Gtk::Widget*> 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 <nicholasbishop@gmail.com>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ * 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 <memory>
+
+#include <gtkmm/notebook.h>
+
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+
+#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<void>& 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<SPFilter*> filter;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> sel;
+ Gtk::TreeModelColumn<int> 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<Gdk::DragContext>& /*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<Gtk::ListStore> _model;
+ Columns _columns;
+ Gtk::CellRendererToggle _cell_toggle;
+ Gtk::Button _add;
+ Gtk::Menu *_menu;
+ sigc::signal<void> _signal_filter_changed;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+ };
+
+ class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ PrimitiveColumns()
+ {
+ add(primitive);
+ add(type_id);
+ add(type);
+ add(id);
+ }
+
+ Gtk::TreeModelColumn<SPFilterPrimitive*> primitive;
+ Gtk::TreeModelColumn<Inkscape::Filters::FilterPrimitiveType> type_id;
+ Gtk::TreeModelColumn<Glib::ustring> type;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ };
+
+ class CellRendererConnection : public Gtk::CellRenderer
+ {
+ public:
+ CellRendererConnection();
+ Glib::PropertyProxy<void*> 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<void*> _primitive;
+ int _text_width;
+ };
+
+ class PrimitiveList : public Gtk::TreeView
+ {
+ public:
+ PrimitiveList(FilterEffectsDialog&);
+
+ sigc::signal<void>& signal_primitive_changed();
+
+ void update();
+ void set_menu(Gtk::Widget &parent,
+ sigc::slot<void> dup,
+ sigc::slot<void> 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<Cairo::Context> &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<Gdk::DragContext>&) override;
+ private:
+ void init_text();
+
+ void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ const bool fill);
+
+ bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector<Gdk::Point>& 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<Cairo::Context>& 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<Gtk::ListStore> _model;
+ PrimitiveColumns _columns;
+ CellRendererConnection _connection_cell;
+ Gtk::Menu *_primitive_menu;
+ Glib::RefPtr<Pango::Layout> _vertical_layout;
+ int _in_drag;
+ SPFilterPrimitive* _drag_prim;
+ sigc::signal<void> _signal_primitive_changed;
+ sigc::connection _scroll_connection;
+ int _autoscroll_y;
+ int _autoscroll_x;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _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<Inkscape::Filters::FilterPrimitiveType> _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 <bryce@bryceharrington.org>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "find.h"
+
+#include <gtkmm/entry.h>
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#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<SPText *>(item) || dynamic_cast<SPFlowtext *>(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<SPText *>(item) || dynamic_cast<SPFlowtext *>(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<SPString *>(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<Inkscape::XML::AttributeRecord const> 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<Glib::ustring> vFontTokenNames;
+ vFontTokenNames.emplace_back("font-family:");
+ vFontTokenNames.emplace_back("-inkscape-font-specification:");
+
+ std::vector<Glib::ustring> 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<SPItem*> Find::filter_fields (std::vector<SPItem*> &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<SPItem*> in = l;
+ std::vector<SPItem*> out;
+
+ if (check_searchin_text.get_active()) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(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<SPRect *>(item)) {
+ return ( all ||check_rects.get_active());
+
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ return ( all || check_ellipses.get_active());
+
+ } else if (dynamic_cast<SPStar *>(item) || dynamic_cast<SPPolygon *>(item)) {
+ return ( all || check_stars.get_active());
+
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ return ( all || check_spirals.get_active());
+
+ } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPLine *>(item) || dynamic_cast<SPPolyLine *>(item)) {
+ return (all || check_paths.get_active());
+
+ } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPTSpan *>(item) ||
+ dynamic_cast<SPTRef *>(item) || dynamic_cast<SPString *>(item) ||
+ dynamic_cast<SPFlowtext *>(item) || dynamic_cast<SPFlowdiv *>(item) ||
+ dynamic_cast<SPFlowtspan *>(item) || dynamic_cast<SPFlowpara *>(item)) {
+ return (all || check_texts.get_active());
+
+ } else if (dynamic_cast<SPGroup *>(item) && !desktop->isLayer(item) ) { // never select layers!
+ return (all || check_groups.get_active());
+
+ } else if (dynamic_cast<SPUse *>(item)) {
+ return (all || check_clones.get_active());
+
+ } else if (dynamic_cast<SPImage *>(item)) {
+ return (all || check_images.get_active());
+
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ return (all || check_offsets.get_active());
+ }
+
+ return false;
+}
+
+std::vector<SPItem*> Find::filter_types (std::vector<SPItem*> &l)
+{
+ std::vector<SPItem*> n;
+ for (std::vector<SPItem*>::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_type_match(item)) {
+ n.push_back(*i);
+ }
+ }
+ return n;
+}
+
+
+std::vector<SPItem*> &Find::filter_list (std::vector<SPItem*> &l, bool exact, bool casematch)
+{
+ l = filter_types (l);
+ l = filter_fields (l, exact, casematch);
+ return l;
+}
+
+std::vector<SPItem*> &Find::all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked)
+{
+ if (dynamic_cast<SPDefs *>(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<SPItem *>(&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<SPItem*> &Find::all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &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<SPItem *>(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<SPItem*> 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<SPItem*> 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("<b>%d</b> object found (out of <b>%d</b>), %s match.",
+ "<b>%d</b> objects found (out of <b>%d</b>), %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<SPItem *>(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 <bryce@bryceharrington.org>
+ *
+ * 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 <gtkmm/box.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/sizegroup.h>
+
+#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<SPItem*> filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch);
+ bool item_type_match (SPItem *item);
+ std::vector<SPItem*> filter_types (std::vector<SPItem*> &l);
+ std::vector<SPItem*> & filter_list (std::vector<SPItem*> &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<SPItem*> & all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked);
+ /**
+ * to return a list of all the selected items
+ *
+ */
+ std::vector<SPItem*> & all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &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<Gtk::CheckButton *> 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<Gtk::SizeGroup> _left_size_group;
+ Glib::RefPtr<Gtk::SizeGroup> _right_size_group;
+
+ /**
+ * A vector of all the check option widgets for easy processing
+ */
+ std::vector<Gtk::CheckButton *> 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 <broberg@kth.se>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/dialog.h>
+#include <glibmm/main.h>
+
+#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<void> FloatingBehavior::signal_show() { return _d->signal_show(); }
+Glib::SignalProxy0<void> FloatingBehavior::signal_hide() { return _d->signal_hide(); }
+Glib::SignalProxy1<bool, GdkEventAny *> 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 <broberg@kth.se>
+ *
+ * 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 <glibmm/property.h>
+#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<void> signal_show() override;
+ Glib::SignalProxy0<void> signal_hide() override;
+ Glib::SignalProxy1<bool, GdkEventAny *> 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<bool> _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 <set>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+
+#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<SPItem*> l = getFontReplacedItems(doc, &out);
+ if (out.length() > 0) {
+ show(out, l);
+ }
+ }
+}
+
+void
+FontSubstitution::show(Glib::ustring out, std::vector<SPItem*> &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<SPItem*> FontSubstitution::getFontReplacedItems(SPDocument* doc, Glib::ustring *out)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ std::vector<SPItem*> allList;
+ std::vector<SPItem*> outList,x,y;
+ std::set<Glib::ustring> setErrors;
+ std::set<Glib::ustring> setFontSpans;
+ std::map<SPItem *, Glib::ustring> 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<SPItem *, Glib::ustring>::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<Glib::ustring> 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<Glib::ustring>::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<Glib::ustring>::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 <glibmm/ustring.h>
+
+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<SPItem*> &l);
+
+ static FontSubstitution &getInstance() { return *new FontSubstitution(); }
+ Glib::ustring getSubstituteFontName (Glib::ustring font);
+
+protected:
+ std::vector<SPItem*> 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 <vector>
+
+#include "glyphs.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#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<GUnicodeScript, Glib::ustring> & getScriptToName()
+{
+ static bool init = false;
+ static std::map<GUnicodeScript, Glib::ustring> 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<gunichar, gunichar> Range;
+typedef std::pair<Range, Glib::ustring> NamedRange;
+
+static std::vector<NamedRange> & getRanges()
+{
+ static bool init = false;
+ static std::vector<NamedRange> 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<gunichar> code;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> 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<Glib::RefPtr<Gtk::TreeModel> >(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<GUnicodeScript, Glib::ustring> 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<GUnicodeScript, Glib::ustring> 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<Gtk::ListStore> 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<gunichar> 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] = "<span font_desc=\"" + fontspec + "\">" + tmp + "</span>";
+ (*row)[columns->tooltip] = "<span font_desc=\"" + fontspec + "\" size=\"42000\">" + tmp + "</span>";
+ }
+
+ // 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 <gtkmm/treemodel.h>
+#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<Gtk::ListStore> 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<sigc::connection> instanceConns;
+ std::vector<sigc::connection> 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 <glibmm/i18n.h>
+
+#include <gtkmm/grid.h>
+
+#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<double> row_heights;
+ std::vector<double> col_widths;
+ std::vector<double> row_ys;
+ std::vector<double> col_xs;
+
+ int NoOfCols = NoOfColsSpinner.get_value_as_int();
+ int NoOfRows = NoOfRowsSpinner.get_value_as_int();
+
+ width = 0;
+ for (a=0;a<NoOfCols; a++){
+ col_widths.push_back(width);
+ }
+
+ height = 0;
+ for (a=0;a<NoOfRows; a++){
+ row_heights.push_back(height);
+ }
+ grid_left = 99999;
+ grid_top = 99999;
+
+ SPDesktop *desktop = Parent->getDesktop();
+ desktop->getDocument()->ensureUpToDate();
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ std::vector<SPItem*> 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<SPItem*> 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<SPItem*> 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;a<NoOfCols; a++){
+ col_widths.push_back(col_width);
+ }
+ } else {
+ for (a = 0; a < (int)col_widths.size(); a++)
+ {
+ total_col_width += col_widths[a] ;
+ }
+ }
+
+ if (RowHeightButton.get_active()){
+ total_row_height = row_height * NoOfRows;
+ row_heights.clear();
+ for (a=0;a<NoOfRows; a++){
+ row_heights.push_back(row_height);
+ }
+ } else {
+ for (a = 0; a < (int)row_heights.size(); a++)
+ {
+ total_row_height += row_heights[a] ;
+ }
+ }
+
+
+ Geom::OptRect sel_bbox = selection->visualBounds();
+ // 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<NoOfCols; a++){
+ if (a<1) col_xs.push_back(0);
+ else col_xs.push_back(col_widths[a-1]+paddingx+col_xs[a-1]);
+ }
+
+
+ for (a=0;a<NoOfRows; a++){
+ if (a<1) row_ys.push_back(0);
+ else row_ys.push_back(row_heights[a-1]+paddingy+row_ys[a-1]);
+ }
+
+ cnt=0;
+ std::vector<SPItem*>::iterator it = sorted.begin();
+ for (row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) {
+
+ std::vector<SPItem *> current_row;
+ col_cnt = 0;
+ for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) {
+ current_row.push_back(*it);
+ col_cnt++;
+ }
+
+ for (auto item:current_row) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ 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<SPItem*> 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<int>(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(" &#215; ");
+ 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 <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/radiobuttongroup.h>
+
+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 <lauris@kaplinski.com>
+ * Andrius R. <knutux@gmail.com>
+ * 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 <glibmm/i18n.h>
+
+#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. <knutux@gmail.com>
+ * 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 <gtkmm/checkbutton.h>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+
+#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 <glibmm/i18n.h>
+#include <glibmm/timer.h>
+#include <glibmm/main.h>
+
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/frame.h>
+
+#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<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default");
+ std::vector<int> 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<int>::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<int>( 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<int>(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<Gdk::Pixbuf> 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 <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/label.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/image.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/toggletoolbutton.h>
+
+#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 <j.b.c.engelen@ewi.utwente.nl>
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * 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 <gio/gio.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/miscutils.h>
+#include <gtk/gtksettings.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/main.h>
+#include <gtkmm/recentinfo.h>
+#include <gtkmm/recentmanager.h>
+
+#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 <fstream>
+
+#if HAVE_ASPELL
+# include "ui/dialog/spellcheck.h" // for get_available_langs
+# ifdef _WIN32
+# include <windows.h>
+# 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<Gtk::TreeSelection> 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,
+ _("<b>No objects selected</b> 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,
+ _("<b>More than one object selected.</b> 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<char>(ifs)), (std::istreambuf_iterator<char>()));
+ 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<Gtk::StyleContext> 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<Glib::ustring> 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<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+ Glib::RefPtr<Gdk::Pixbuf> 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<Glib::ustring> labels;
+ std::vector<Glib::ustring> 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<Glib::ustring> labels;
+ std::vector<Glib::ustring> 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 <rect>). 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<Glib::ustring> names = ::Inkscape::CMSSystem::getDisplayNames();
+ Glib::ustring current = prefs->getString( "/options/displayprofile/uri" );
+
+ gint index = 0;
+ _cms_display_profile.append(_("<none>"));
+ 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, _("&gt; and &lt; _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<Glib::ustring> fileNames;
+ std::vector<Glib::ustring> 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<Gtk::Window *>(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<Gtk::CellRendererAccel *>(renderer);
+ if (user_set) {
+ accel->property_markup() = Glib::ustring("<span foreground=\"blue\"> " + shortcut + " </span>").c_str();
+ } else {
+ accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").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::vector<Verb *>verbs = 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<Glib::ustring> languages;
+ std::vector<Glib::ustring> 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<Gtk::RecentManager> 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<Gtk::TreeSelection> 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("<span size='large'><b>" + col_name_escaped + "</b></span>");
+ _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 <j.b.c.engelen@ewi.utwente.nl>
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * 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 <iostream>
+#include <vector>
+#include "ui/widget/preferences-widget.h"
+#include <cstddef>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelfilter.h>
+
+#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<Gtk::TreeStore> _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<Glib::ustring> _col_name;
+ Gtk::TreeModelColumn<int> _col_id;
+ Gtk::TreeModelColumn<UI::Widget::DialogPage*> _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<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> shortcut;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<unsigned int> shortcutid;
+ Gtk::TreeModelColumn<unsigned int> user_set;
+ };
+ ModelColumns _kb_columns;
+ static ModelColumns &onKBGetCols();
+ Glib::RefPtr<Gtk::TreeStore> _kb_store;
+ Gtk::TreeView _kb_tree;
+ Gtk::CellRendererAccel _kb_shortcut_renderer;
+ Glib::RefPtr<Gtk::TreeModelFilter> _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 <map>
+#include <set>
+#include <list>
+#include "ui/widget/panel.h"
+#include "ui/widget/frame.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/cellrenderercombo.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/progressbar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/eventbox.h>
+
+#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<bool> toggler;
+ Gtk::TreeModelColumn<Glib::ustring> expander;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > thumbnail;
+ Gtk::TreeModelColumn<Glib::RefPtr<InputDevice const> > device;
+ Gtk::TreeModelColumn<Gdk::InputMode> mode;
+
+ DeviceModelColumns() { add(toggler), add(expander), add(description); add(thumbnail); add(device); add(mode); }
+};
+
+static std::map<Gdk::InputMode, Glib::ustring> &getModeToString()
+{
+ static std::map<Gdk::InputMode, Glib::ustring> 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<Glib::ustring, Gdk::InputMode> &getStringToMode()
+{
+ static std::map<Glib::ustring, Gdk::InputMode> 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<Gtk::TreeStore> store);
+ static void setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter);
+
+ static void commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> 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<Gtk::TreeStore> 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<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> value;
+ };
+
+ KeysColumns keysColumns;
+ KeysColumns axisColumns;
+
+ Glib::RefPtr<Gtk::ListStore> axisStore;
+ Gtk::TreeView axisTree;
+ Gtk::ScrolledWindow axisScroll;
+
+ Glib::RefPtr<Gtk::ListStore> 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<Gdk::Pixbuf> getPix(PixId id);
+
+ std::map<Glib::ustring, std::set<guint> > buttonMap;
+ std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap;
+
+ GdkInputSource lastSourceSeen;
+ Glib::ustring lastDevnameSeen;
+
+ Glib::RefPtr<Gtk::TreeStore> 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<Gtk::TreeStore> 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<InputDevice const> device);
+ void updateDeviceAxes(Glib::RefPtr<InputDevice const> device);
+ void updateDeviceButtons(Glib::RefPtr<InputDevice const> device);
+ static void updateDeviceLinks(Glib::RefPtr<InputDevice const> 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<Gdk::Pixbuf> InputDialogImpl::getPix(PixId id)
+{
+ static std::map<PixId, Glib::RefPtr<Gdk::Pixbuf> > 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<Gdk::Pixbuf> 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<Glib::RefPtr<InputDevice const> > devices;
+};
+
+static Glib::ustring getCommon( std::list<Glib::ustring> 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<Gtk::TreeSelection> sel = confDeviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = sel->get_selected();
+ if (iter) {
+ Glib::RefPtr<InputDevice const> 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<Gtk::TreeStore> store, Gtk::TreeIter &tablet )
+{
+ std::list<Glib::RefPtr<InputDevice const> > 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<TabletTmp> tablets;
+ std::set<Glib::ustring> 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<Glib::ustring> 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<Glib::RefPtr<InputDevice const> >::iterator it2 = it.devices.begin(); it2 != it.devices.end(); ++it2 ) {
+ Glib::RefPtr<InputDevice const> 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<Glib::ustring> 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<Gtk::CellRendererCombo *>(rndr);
+ if (combo) {
+ Glib::RefPtr<InputDevice const> 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<Gtk::TreeStore> store)
+{
+ Gtk::TreeIter iter = store->get_iter(path);
+ if (iter) {
+ Glib::RefPtr<InputDevice const> 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<Gtk::CellRendererToggle *>(rndr);
+ if (toggle) {
+ Glib::RefPtr<InputDevice const> 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<Gtk::TreeStore> store)
+{
+ Gtk::TreeIter iter = store->get_iter(path);
+ if (iter) {
+ Glib::RefPtr<InputDevice const> 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<Gtk::TreeSelection> 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<InputDevice const> dev = row[getCols().device];
+ Gdk::InputMode mode = (*iter)[getCols().mode];
+ modeCombo.set_active(getModeId(mode));
+
+ titleLabel.set_markup("<b>" + row[getCols().description] + "</b>");
+
+ 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<InputDevice const> device)
+{
+// g_message("OUCH!!!! for %p hits %s", &device, device->getId().c_str());
+ std::vector<Glib::RefPtr<Gtk::TreeStore> > stores;
+ stores.push_back(deviceStore);
+ stores.push_back(cfgPanel.confDeviceStore);
+
+ for (auto & store : stores) {
+ Gtk::TreeModel::iterator deviceIter;
+ store->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>(
+ 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<InputDevice const> device)
+{
+ gint live = device->getLiveAxes();
+
+ std::map<guint, std::pair<guint, gdouble> > 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<InputDevice const> device)
+{
+ gint live = device->getLiveButtons();
+ std::set<guint> 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<InputDevice const> 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<InputDevice const> dev = (*iter)[getCols().device];
+ if ( dev && (dev->getLink() == link) ) {
+ if ( result ) {
+ *result = iter;
+ }
+ stop = true;
+ }
+ return stop;
+}
+
+void InputDialogImpl::updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree)
+{
+ Glib::RefPtr<Gtk::TreeStore> deviceStore = Glib::RefPtr<Gtk::TreeStore>::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<Glib::ustring, Gtk::TreeModel::iterator*>(
+ 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<InputDevice const> dev = (*deviceIter)[getCols().device];
+ Glib::ustring descr = (*deviceIter)[getCols().description];
+ Glib::RefPtr<Gdk::Pixbuf> 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<InputDevice const> dev = (*deviceIter)[getCols().device];
+ Glib::ustring descr = (*deviceIter)[getCols().description];
+ Glib::RefPtr<Gdk::Pixbuf> 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<Glib::ustring, Gtk::TreeModel::iterator*>(
+ 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<Gtk::TreeSelection> 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<InputDevice const> 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<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices();
+ for ( std::list<Glib::RefPtr<InputDevice const> >::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<Gtk::TreeSelection> 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<InputDevice const> 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<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices();
+ for ( std::list<Glib::RefPtr<InputDevice const> >::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<gint>(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<gint>(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<Gtk::TreeSelection> 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<InputDevice const> idev = row[getCols().device];
+ if ( !idev || (idev->getId() != key) ) {
+ dev = nullptr;
+ }
+ }
+ }
+
+ for ( gint i = 0; i < static_cast<gint>(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<gint>(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<gint>(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<gint>(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<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > 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<GdkEventKey*>(event);
+ gchar* name = gtk_accelerator_name(keyEvt->keyval, static_cast<GdkModifierType>(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<GdkEventButton*>(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<GdkModifierType>(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<GdkEventMotion*>(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<GdkModifierType>(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 <bryce@bryceharrington.com>
+ * Andrius R. <knutux@gmail.com>
+ * 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 <boost/lexical_cast.hpp>
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#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<void*, void>(&::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<SPKnot *>(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 <bryce@bryceharrington.com>
+ *
+ * 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 <gtkmm/dialog.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/spinbutton.h>
+#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 <bryce@bryceharrington.org>
+ * Andrius R. <knutux@gmail.com>
+ * 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 <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#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<void*, void>(&::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<Gtk::TreeSelection> 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 <bryce@bryceharrington.org>
+ *
+ * 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 <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+#include <gtkmm/grid.h>
+
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#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<LayerRelativePosition> position;
+ Gtk::TreeModelColumn<Glib::ustring> 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<SPObject*> _colObject;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<bool> _colVisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ };
+
+ Gtk::TreeView _tree;
+ ModelColumns* _model;
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ Gtk::ScrolledWindow _scroller;
+
+
+ PositionDropdownColumns _dropdown_columns;
+ Gtk::CellRendererText _label_renderer;
+ Glib::RefPtr<Gtk::ListStore> _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 <gtkmm/icontheme.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+
+#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<SPObject*> _colObject;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<bool> _colVisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+};
+
+void LayersPanel::_updateLayer( SPObject *layer ) {
+ _store->foreach( sigc::bind<SPObject*>(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<SPObject*>(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<Gtk::TreeSelection> 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<Gtk::TreeSelection> 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<GdkEvent*>(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<int>(event->x);
+ int y = static_cast<int>(event->y);
+ if ( _tree.get_path_at_pos( x, y, path ) ) {
+ _checkTreeSelection();
+
+ _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(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<int>(event->x);
+ int y = static_cast<int>(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<int>(event->x);
+ int y = static_cast<int>(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<Gdk::DragContext>& /*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<SPItem*>(_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<Gtk::TreeModel> 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<GdkEventButton const*>(_toggleEvent);
+ GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(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<ContextMenu*>(&_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 <gtkmm/box.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/scrolledwindow.h>
+#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<Gdk::DragContext>& 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<Gtk::TreeModel> 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<Gtk::TreeStore> _store;
+ std::vector<Gtk::Widget*> _watching;
+ std::vector<Gtk::Widget*> _watchingNonTop;
+ std::vector<Gtk::Widget*> _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 <cmath>
+#include <glibmm/i18n.h>
+
+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<int>(converter._length); ++i) {
+ Glib::RefPtr<Gtk::Builder> 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<LivePathEffect::EffectType> *data = &converter.data(i);
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ LPESelectorEffect->signal_button_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ 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<Glib::RefPtr<Gtk::Builder>>(sigc::mem_fun(*this, &LivePathEffectAdd::expand), builder_effect));
+ LPESelectorEffectEventExpander->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffectEventExpander->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ 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<span size='x-small'>" + untranslated_label + "</span>").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<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect));
+ Gtk::EventBox *LPESelectorEffectEventFav;
+ builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav);
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(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<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ Gtk::Image *favtop = dynamic_cast<Gtk::Image *>(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<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i)));
+ LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj())));
+ LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ 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<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ _LPESelectorFlowBox->insert(*LPESelectorEffect, i);
+ LPESelectorEffect->get_parent()->signal_key_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ 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<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj())));
+ _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ 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<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj())));
+ _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ 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<Gtk::Widget *>(_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<LivePathEffect::EffectType> *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<Gtk::FlowBoxChild *> 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<Gtk::FlowBoxChild *>(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<Gtk::FlowBoxChild *>(i);
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(leitem->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Box *actions = dynamic_cast<Gtk::Box *>(contents[5]);
+ if (actions) {
+ actions->set_visible(false);
+ }
+ Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (expander) {
+ expander->set_visible(true);
+ }
+ }
+ }
+ }
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (expander) {
+ expander->set_visible(false);
+ }
+ }
+ }
+
+ child->show_all_children();
+ _LPESelectorFlowBox->select_child(*child);
+ }
+}
+
+bool LivePathEffectAdd::pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> 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<Gtk::Builder> 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<Gtk::Image *>(_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<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add)
+{
+ _to_add = to_add;
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(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<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *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<Gtk::FlowBoxChild *>(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<Gtk::Builder> builder_effect)
+{
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ if (child) {
+ child->grab_focus();
+ }
+ return true;
+}
+
+
+
+bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child)
+{
+ std::vector<Glib::ustring> 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<LivePathEffect::EffectType> *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<Gtk::EventBox *>(child->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ std::vector<Gtk::Widget *> content_overlay = overlay->get_children();
+
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ if (!sp_has_fav(lpename->get_text()) && _showfavs) {
+ return false;
+ }
+ Gtk::ToggleButton *experimental = dynamic_cast<Gtk::ToggleButton *>(contents[3]);
+ if (experimental) {
+ if (experimental->get_active() && !_LPEExperimental->get_active()) {
+ return false;
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(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<Gtk::EventBox *>(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<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(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<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(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<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(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<Gtk::EventBox *>(contents_overlay[1]);
+ if (LPESelectorEffectEventFavTop) {
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(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<Gtk::EventBox *>(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<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(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<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(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<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(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<Gtk::EventBox *>(contents_overlay[1]);
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(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<Glib::ustring> 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<SPShape *>(item);
+ SPPath *path = dynamic_cast<SPPath *>(item);
+ SPGroup *group = dynamic_cast<SPGroup *>(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<Gtk::Adjustment> 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 <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/flowboxchild.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/overlay.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/stylecontext.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/viewport.h>
+
+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<LivePathEffect::EffectType> *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<Gtk::Builder> builder_effect);
+ bool hide_pop_description(GdkEventCrossing *evt);
+ bool fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect);
+ bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ bool on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ bool expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> 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<Gtk::Builder> _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<LivePathEffect::EffectType> *_to_add;
+ bool _showfavs;
+ bool _applied;
+ class Effect;
+ const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &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 <j.b.c.engelen@utwente.nl>
+ * Steren Giannini <steren.giannini@gmail.com>
+ * Bastien Bouclet <bgkweb@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "livepatheffect-editor.h"
+
+#include <gtkmm/expander.h>
+
+#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<LivePathEffectEditor *>(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<LivePathEffectEditor *>(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<Gtk::TreeSelection> 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<SPLPEItem *>(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<SPUse *>(item);
+ if ( use ) {
+ // test whether linked object is supported by the CLONE_ORIGINAL LPE
+ SPItem *orig = use->get_original();
+ if ( dynamic_cast<SPShape *>(orig) ||
+ dynamic_cast<SPGroup *>(orig) ||
+ dynamic_cast<SPText *>(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<SPLPEItem *>(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<LivePathEffect::EffectType> *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<SPUse *>(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<SPShape *>(orig) ||
+ dynamic_cast<SPGroup *>(orig) ||
+ dynamic_cast<SPText *>(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<SPLPEItem *>(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<SPLPEItem *>(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<SPLPEItem *>(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<Gtk::TreeSelection> 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<SPLPEItem *>(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 <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * 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 <gtkmm/label.h>
+#include <gtkmm/frame.h>
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/frame.h"
+#include "object/sp-item.h"
+#include "live_effects/effect-enum.h"
+#include <gtkmm/liststore.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/toolbar.h>
+#include <gtkmm/buttonbox.h>
+#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<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<LivePathEffect::LPEObjectReference *> lperef;
+ Gtk::TreeModelColumn<bool> col_visible;
+ };
+
+ bool lpe_list_locked;
+ //Inkscape::UI::Widget::ComboBoxEnum<LivePathEffect::EffectType> 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<Gtk::ListStore> effectlist_store;
+ Glib::RefPtr<Gtk::TreeSelection> 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 <gtkmm.h>
+#include "lpe-fillet-chamfer-properties.h"
+#include <boost/lexical_cast.hpp>
+#include <glibmm/i18n.h>
+#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<void*, void>(&::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 <gtkmm.h>
+#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 <bryce@bryceharrington.com>
+ * Andrius R. <knutux@gmail.com>
+ * 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 <boost/lexical_cast.hpp>
+#include <glibmm/i18n.h>
+#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<void*, void>(&::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<Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *>(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 <bryce@bryceharrington.com>
+ *
+ * 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 <gtkmm.h>
+#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 <mental@rydia.net>
+ *
+ * Copyright (C) 2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/dialog/memory.h"
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/dialog.h>
+
+#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<char> Digits;
+ typedef std::vector<Digits *> 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<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> used;
+ Gtk::TreeModelColumn<Glib::ustring> slack;
+ Gtk::TreeModelColumn<Glib::ustring> 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<Gtk::ListStore> 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 <mental@rydia.net>
+ *
+ * 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<Gtk::TextBuffer> 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<Gtk::TextBuffer> 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<Messages *>(user_data);
+ dlg->message(const_cast<char*>(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 <gtkmm/box.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include <glibmm/i18n.h>
+
+#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 <jan.darowski@gmail.com>, 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 <jan.darowski@gmail.com>, 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 <gtkmm/dialog.h>
+#include <gtkmm/button.h>
+
+#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 <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <glibmm/i18n.h>
+
+#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<Glib::ustring> labels;
+ std::vector<Glib::ustring> 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 <Kris.DeGussem@gmail.com>
+ *
+ * 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 <Kris.DeGussem@gmail.com>
+ * c++ version based on former C-version (GPL v2+) with authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Abhishek Sharma
+ */
+
+#include "object-properties.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/grid.h>
+
+#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<SPObject*>(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<SPObject*>(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 <Kris.DeGussem@gmail.com>
+ * c++version based on former C-version (GPL v2+) with authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * 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 <gtkmm/checkbutton.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/comboboxtext.h>
+
+#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<Glib::ustring> _int_attrs;
+ std::vector<Glib::ustring> _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 <flutterguy317@gmail.com>
+ * Tavmjong Bah 2017
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+
+#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<SPItem*> _colObject;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<bool> _colVisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ Gtk::TreeModelColumn<int> _colType;
+ Gtk::TreeModelColumn<guint32> _colHighlight;
+ Gtk::TreeModelColumn<int> _colClipMask;
+ Gtk::TreeModelColumn<bool> _colPrevSelectionState;
+ //Gtk::TreeModelColumn<int> _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<bool *>(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<Gtk::TreeSelection> 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<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass));
+ first_pass = false;
+ _store->foreach_iter(sigc::bind<bool *>(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<int>(event->x);
+ int y = static_cast<int>(event->y);
+ if ( _tree.get_path_at_pos( x, y, path ) ) {
+ _checkTreeSelection();
+ _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(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<int>(event->x);
+ int y = static_cast<int>(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<int>(event->x);
+ int y = static_cast<int>(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<bool>(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<bool>(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<int>(event->x);
+ int y = static_cast<int>(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<Gdk::DragContext>& /*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<gchar *> 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<Gtk::TreeModel> 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<Gtk::TreeModel> 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<GdkEventButton const*>(_toggleEvent);
+ GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(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<double>(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<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false));
+ _tree.signal_row_expanded().connect( sigc::bind<bool>(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<ContextMenu*>(&_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<NodeTool*>(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<NodeTool*>(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 <flutterguy317@gmail.com>
+ * 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 <gtkmm/box.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/dialog.h>
+#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<SPItem*, std::pair<ObjectsPanel::ObjectWatcher*, bool> > _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<SPItem*> _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<SPItem*> _highlight_target;
+
+ //Show icons in the context menu
+ bool _show_contextmenu_icons;
+
+ //GUI Members:
+
+ GdkEvent* _toggleEvent;
+
+ Gtk::TreeModel::Path _defer_target;
+
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ std::list<std::tuple<SPItem*, Gtk::TreeModel::iterator, bool> > _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<SPItem*, Gtk::TreeModel::iterator> _tree_cache;
+ std::list<SPItem *> _selected_objects_order; // ordered by time of selection
+ std::list<Gtk::TreePath> _paths_to_be_expanded;
+
+ std::vector<Gtk::Widget*> _watching;
+ std::vector<Gtk::Widget*> _watchingNonTop;
+ std::vector<Gtk::Widget*> _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<Inkscape::UI::SelectedColor> _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<Gdk::DragContext>& 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<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b );
+ bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> 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 <algorithm>
+#include <iostream>
+#include <map>
+#include <utility>
+
+#include <giomm/listmodel.h>
+#include <glibmm/regex.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/stockid.h>
+#include <gtkmm/switch.h>
+
+#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"=====(
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs id="Defs"/>
+ <rect id="Back" x="0" y="0" width="100px" height="100px" fill="lightgray"/>
+ <rect id="Rect" x="0" y="0" width="100px" height="100px" stroke="black"/>
+</svg>
+)=====";
+
+class PaintServersColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> paint;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf;
+ Gtk::TreeModelColumn<Glib::ustring> 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<Glib::RefPtr<Gtk::TreeModel>>(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<Glib::Regex> regex1 = Glib::Regex::create(":(url\\(#([A-z0-9\\-_\\.#])*\\))");
+ regex1->match(paint, matchInfo);
+
+ if (matchInfo.matches()) {
+ return matchInfo.fetch(1);
+ }
+
+ // Color
+ static Glib::RefPtr<Glib::Regex> 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<Glib::ustring>& list)
+{
+
+ g_return_if_fail(in != nullptr);
+
+ // Add paint servers in <defs> section.
+ if (dynamic_cast<SPPaintServer *>(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<SPShape *>(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<Gdk::Pixbuf> 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<Gdk::Pixbuf> 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<Glib::Regex> 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<SPObject *> 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<SPItem *>(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<Glib::ustring> 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<Gdk::Pixbuf> 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<PaintServersColumns> columns(getColumns());
+ SPDocument *document = desktop->getDocument();
+ Glib::RefPtr<Gtk::ListStore> current = store[CURRENTDOC];
+
+ std::vector<Glib::ustring> paints;
+ std::vector<Glib::ustring> paints_current;
+ std::vector<Glib::ustring> 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<Gdk::Pixbuf> 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<SPObject*> 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<Gdk::Pixbuf> 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<SPObject*> items;
+ for (auto item : selected_items) {
+ std::vector<SPObject*> 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<SPObject*> PaintServersDialog::extract_elements(SPObject* item)
+{
+ std::vector<SPObject*> elements;
+ std::vector<SPObject*> children = item->childList(false);
+ if (!children.size()) {
+ elements.push_back(item);
+ } else {
+ for (auto e : children) {
+ std::vector<SPObject*> 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 <glibmm/i18n.h>
+#include <gtkmm.h>
+
+#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<Gdk::Pixbuf> 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<SPObject *> extract_elements(SPObject *item);
+
+ const Glib::ustring ALLDOCS;
+ const Glib::ustring CURRENTDOC;
+ std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store;
+ Glib::ustring current_store;
+ std::map<Glib::ustring, SPDocument *> 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 <broberg@kth.se>
+ * C 2012 Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_PANEL_DIALOG_H
+#define INKSCAPE_PANEL_DIALOG_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm/dialog.h>
+
+#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 <typename Behavior>
+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 <typename T>
+ static PanelDialog<Behavior> *create();
+
+ inline void present() override;
+
+private:
+ template <typename T>
+ static PanelDialog<Behavior> *_create();
+
+ inline void _presentDialog();
+
+ PanelDialog() = delete;
+ PanelDialog(PanelDialog<Behavior> const &d) = delete; // no copy
+ PanelDialog<Behavior>& operator=(PanelDialog<Behavior> 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 <typename B>
+PanelDialog<B>::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 <typename B> template <typename P>
+PanelDialog<B> *PanelDialog<B>::create()
+{
+ return _create<P>();
+}
+
+template <typename B> template <typename P>
+PanelDialog<B> *PanelDialog<B>::_create()
+{
+ UI::Widget::Panel &panel = P::getInstance();
+ return new PanelDialog<B>(panel, panel.getPrefsPath(), panel.getVerb());
+}
+
+template <typename B>
+void PanelDialog<B>::present()
+{
+ _panel.present();
+}
+
+template <typename B>
+void PanelDialog<B>::_presentDialog()
+{
+ Dialog::present();
+}
+
+template <> inline
+void PanelDialog<Behavior::FloatingBehavior>::present()
+{
+ Dialog::present();
+ _panel.present();
+}
+
+template <> inline
+void PanelDialog<Behavior::FloatingBehavior>::_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 <typename P>
+PanelDialog<Behavior::FloatingBehavior> *PanelDialog<Behavior::FloatingBehavior>::create()
+{
+ auto instance = _create<P>();
+
+ INKSCAPE.signal_activate_desktop.connect(
+ sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_propagateDesktopActivated)
+ );
+ INKSCAPE.signal_deactivate_desktop.connect(
+ sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_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 <glibmm/i18n.h>
+#include <gtkmm/messagedialog.h>
+
+#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<SPItem*> 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 <gtkmm/radiobutton.h>
+#include <gtkmm/radiobuttongroup.h>
+#include <gtkmm/grid.h>
+
+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 <juca@members.fsf.org>
+ *
+ * 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 <glibmm/i18n.h>
+
+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 <juca@members.fsf.org>
+ *
+ * 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 <gtkmm/box.h>
+
+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 <kees@outflux.net>
+ * 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 <cmath>
+
+#include <gtkmm.h>
+
+#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 <glibmm/i18n.h>
+
+
+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<Gtk::PageSetup> 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<Gtk::PaperSize> 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<Gtk::PrintContext>& 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<SPItem*>());
+
+ // 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<Cairo::ImageSurface> 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<Gtk::PrintContext>&)
+{
+ //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 <kees@outflux.net>
+ *
+ * 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/printoperation.h> // 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::PrintSettings> _gtk_print_settings;
+};
+
+class Print {
+public:
+ Print(SPDocument *doc, SPItem *base);
+ Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window);
+
+protected:
+
+private:
+ Glib::RefPtr<Gtk::PrintOperation> _printop;
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions _tab;
+
+ struct workaround_gtkmm _workaround;
+
+ void draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/);
+ Gtk::Widget *create_custom_widget();
+ void begin_print(const Glib::RefPtr<Gtk::PrintContext>&);
+};
+
+} // 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 <glibmm/i18n.h>
+
+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 <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/checkbutton.h>
+
+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 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * 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 <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include <map>
+#include <regex>
+#include <utility>
+
+// 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<SelectorsDialog::TreeStore *>(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> 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<SelectorsDialog::TreeStore>(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<Glib::Regex> regex1 =
+ // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}");
+ //
+ // Glib::MatchInfo minfo;
+ // regex1->match(content, minfo);
+
+ // Split on curly brackets. Even tokens are selectors, odd are values.
+ std::vector<Glib::ustring> 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<std::pair<Glib::ustring, bool>> 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<Glib::ustring> 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<SPObject *> 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<Glib::ustring> 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<Glib::ustring> 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<Glib::ustring> 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<SPObject *>(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<Glib::ustring> 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<Glib::ustring, Glib::ustring> result;
+ std::vector<Glib::ustring> 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<SPObject *> toAddObjVec(selection->objects().begin(), selection->objects().end());
+ Glib::ustring multiselector = row[_mColumns._colSelector];
+ std::vector<SPObject *> objVec = _getObjVec(multiselector);
+ row[_mColumns._colObj] = objVec;
+ row[_mColumns._colExpand] = true;
+ std::vector<Glib::ustring> 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<SPObject *> 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<SPObject *> 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<SPObject *>(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<AttributeRecord const> 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<Glib::ustring> 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<SPObject *> 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<SPObject *> 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<SPObject *> &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<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
+ std::sort(tokens.begin(), tokens.end());
+ tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end());
+ std::vector<Glib::ustring> 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<SPObject *> &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<Glib::ustring> 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<SPObject *> 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<SPObject *> 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<Gtk::Dialog *>(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<SPObject *> 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<Glib::ustring> 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<SPObject *> 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<SPObject *>(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<Gtk::TreeSelection> 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<int>(event->x);
+ int y = static_cast<int>(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<int>(event->x);
+ int y = static_cast<int>(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<Gtk::TreeModel::Path> 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<SPObject *> 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 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * 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 <gtkmm/dialog.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treeselection.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <ui/widget/panel.h>
+
+#include "xml/helper-observer.h"
+
+#include <memory>
+#include <vector>
+
+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 <style> element for changes.
+ class NodeObserver;
+
+ // Monitor all objects for addition/removal/attribute change
+ class NodeWatcher;
+ enum SelectorType { CLASS, ID, TAG };
+ void _nodeAdded( Inkscape::XML::Node &repr );
+ void _nodeRemoved( Inkscape::XML::Node &repr );
+ void _nodeChanged( Inkscape::XML::Node &repr );
+ // Data structure
+ enum coltype { OBJECT, SELECTOR, OTHER };
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns() {
+ add(_colSelector);
+ add(_colExpand);
+ add(_colType);
+ add(_colObj);
+ add(_colProperties);
+ add(_colVisible);
+ add(_colSelected);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Selector or matching object id.
+ Gtk::TreeModelColumn<bool> _colExpand; // Open/Close store row.
+ Gtk::TreeModelColumn<gint> _colType; // Selector row or child object row.
+ Gtk::TreeModelColumn<std::vector<SPObject *> > _colObj; // List of matching objects.
+ Gtk::TreeModelColumn<Glib::ustring> _colProperties; // List of properties.
+ Gtk::TreeModelColumn<bool> _colVisible; // Make visible or not.
+ Gtk::TreeModelColumn<gint> _colSelected; // Make selected.
+ };
+ ModelColumns _mColumns;
+
+ // Override Gtk::TreeStore to control drag-n-drop (only allow dragging and dropping of selectors).
+ // See: https://developer.gnome.org/gtkmm-tutorial/stable/sec-treeview-examples.html.en
+ //
+ // TreeStore implements simple drag and drop (DND) but there appears no way to know when a DND
+ // has been completed (other than doing the whole DND ourselves). As a hack, we use
+ // on_row_deleted to trigger write of style element.
+ class TreeStore : public Gtk::TreeStore {
+ protected:
+ TreeStore();
+ bool row_draggable_vfunc(const Gtk::TreeModel::Path& path) const override;
+ bool row_drop_possible_vfunc(const Gtk::TreeModel::Path& path,
+ const Gtk::SelectionData& selection_data) const override;
+ void on_row_deleted(const TreeModel::Path& path) override;
+
+ public:
+ static Glib::RefPtr<SelectorsDialog::TreeStore> create(SelectorsDialog *styledialog);
+
+ private:
+ SelectorsDialog *_selectorsdialog;
+ };
+
+ // TreeView
+ Glib::RefPtr<Gtk::TreeModelFilter> _modelfilter;
+ Glib::RefPtr<TreeStore> _store;
+ Gtk::TreeView _treeView;
+ Gtk::TreeModel::Path _lastpath;
+ // Widgets
+ StyleDialog *_style_dialog;
+ Gtk::Paned _paned;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Box _button_box;
+ Gtk::Box _selectors_box;
+ Gtk::ScrolledWindow _scrolled_window_selectors;
+
+ Gtk::Button _del;
+ Gtk::Button _create;
+ // Reading and writing the style element.
+ Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false);
+ void _readStyleElement();
+ void _writeStyleElement();
+
+ // Update watchers
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher;
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher;
+ void _updateWatchers(SPDesktop *);
+
+ // Manipulate Tree
+ void _addToSelector(Gtk::TreeModel::Row row);
+ void _removeFromSelector(Gtk::TreeModel::Row row);
+ Glib::ustring _getIdList(std::vector<SPObject *>);
+ std::vector<SPObject *> _getObjVec(Glib::ustring selector);
+ void _insertClass(const std::vector<SPObject *>& objVec, const Glib::ustring& className);
+ void _insertClass(SPObject *obj, const Glib::ustring &className);
+ void _removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all = false);
+ void _removeClass(SPObject *obj, const Glib::ustring &className, bool all = false);
+ void _toggleDirection(Gtk::RadioButton *vertical);
+ void _showWidgets();
+ void _resized();
+ void _childresized();
+ void _panedresized(Gtk::Allocation allocation);
+
+ void _selectObjects(int, int);
+ // Variables
+ double _scroolpos;
+ bool _scroollock;
+ bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+ Inkscape::XML::Node *m_root = nullptr;
+ Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver.
+
+ // Signals and handlers - External
+ sigc::connection _document_replaced_connection;
+ sigc::connection _desktop_changed_connection;
+ sigc::connection _selection_changed_connection;
+
+ void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document);
+ void _handleDesktopChanged(SPDesktop* desktop);
+ void _handleSelectionChanged();
+ void _panedrealized();
+ void _rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _closeDialog(Gtk::Dialog *textDialogPtr);
+
+ DesktopTracker _desktopTracker;
+
+ Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change).
+
+ // Signal and handlers - Internal
+ void _addSelector();
+ void _delSelector();
+ bool _handleButtonEvent(GdkEventButton *event);
+ void _buttonEventsSelectObjs(GdkEventButton *event);
+ void _selectRow(); // Select row in tree when selection changed.
+ void _vscrool();
+
+ // GUI
+ void _styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip);
+};
+
+} // namespace Dialogc
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SELECTORSDIALOG_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/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp
new file mode 100644
index 0000000..44e8302
--- /dev/null
+++ b/src/ui/dialog/spellcheck.cpp
@@ -0,0 +1,815 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Spellcheck dialog.
+ */
+/* Authors:
+ * bulia byak <bulia@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 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 "spellcheck.h"
+#include "message-stack.h"
+
+#include "inkscape.h"
+#include "document.h"
+#include "desktop.h"
+
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK
+#include "ui/tools-switch.h"
+#include "ui/tools/text-tool.h"
+
+#include "text-editing.h"
+#include "selection-chemistry.h"
+#include "display/curve.h"
+#include "document-undo.h"
+#include "verbs.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+#include "object/sp-text.h"
+#include "object/sp-tref.h"
+
+#include <glibmm/i18n.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * Get the list of installed aspell dictionaries/languages
+ */
+std::vector<std::string> SpellCheck::get_available_langs()
+{
+ std::vector<std::string> langs;
+
+#if HAVE_ASPELL
+ auto *config = new_aspell_config();
+ auto const *dlist = get_aspell_dict_info_list(config);
+ auto *elements = aspell_dict_info_list_elements(dlist);
+
+ for (AspellDictInfo const *entry; (entry = aspell_dict_info_enumeration_next(elements)) != nullptr;) {
+ // skip duplicates (I get "de_DE" twice)
+ if (!langs.empty() && langs.back() == entry->name) {
+ continue;
+ }
+
+ langs.emplace_back(entry->name);
+ }
+
+ delete_aspell_dict_info_enumeration(elements);
+ delete_aspell_config(config);
+#endif
+
+ return langs;
+}
+
+static void show_spellcheck_preferences_dialog()
+{
+ Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK);
+ SP_ACTIVE_DESKTOP->_dlg_mgr->showDialog("InkscapePreferences");
+}
+
+SpellCheck::SpellCheck () :
+ UI::Widget::Panel("/dialogs/spellcheck/", SP_VERB_DIALOG_SPELLCHECK),
+ _text(nullptr),
+ _layout(nullptr),
+ _stops(0),
+ _adds(0),
+ _working(false),
+ _local_change(false),
+ _prefs(nullptr),
+ accept_button(_("_Accept"), true),
+ ignoreonce_button(_("_Ignore once"), true),
+ ignore_button(_("_Ignore"), true),
+ add_button(_("A_dd"), true),
+ dictionary_label(_("Language")),
+ dictionary_hbox(false, 0),
+ stop_button(_("_Stop"), true),
+ start_button(_("_Start"), true),
+ desktop(nullptr),
+ deskTrack()
+{
+ _prefs = Inkscape::Preferences::get();
+
+ // take languages from prefs
+ for (const char *langkey : { "lang", "lang2", "lang3" }) {
+ auto lang = _prefs->getString(_prefs_path + langkey);
+ if (!lang.empty()) {
+ _langs.push_back(lang);
+ }
+ }
+
+ banner_hbox.set_layout(Gtk::BUTTONBOX_START);
+ banner_hbox.add(banner_label);
+
+ if (_langs.empty()) {
+ _langs = get_available_langs();
+
+ if (_langs.empty()) {
+ banner_label.set_markup("<i>No aspell dictionaries installed</i>");
+ }
+ }
+
+ scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrolled_window.set_shadow_type(Gtk::SHADOW_IN);
+ scrolled_window.set_size_request(120, 96);
+ scrolled_window.add(tree_view);
+
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ tree_view.append_column(_("Suggestions:"), tree_columns.suggestions);
+
+ if (!_langs.empty()) {
+ for (auto const &lang : _langs) {
+ dictionary_combo.append(lang);
+ }
+ dictionary_combo.set_active(0);
+ }
+
+ accept_button.set_tooltip_text(_("Accept the chosen suggestion"));
+ ignoreonce_button.set_tooltip_text(_("Ignore this word only once"));
+ ignore_button.set_tooltip_text(_("Ignore this word in this session"));
+ add_button.set_tooltip_text(_("Add this word to the chosen dictionary"));
+ pref_button.set_tooltip_text(_("Preferences"));
+ pref_button.set_image_from_icon_name("preferences-system");
+
+ dictionary_hbox.pack_start(dictionary_label, false, false, 6);
+ dictionary_hbox.pack_start(dictionary_combo, true, true, 0);
+ dictionary_hbox.pack_start(pref_button, false, false, 0);
+
+ changebutton_vbox.set_spacing(4);
+ changebutton_vbox.pack_start(accept_button, false, false, 0);
+ changebutton_vbox.pack_start(ignoreonce_button, false, false, 0);
+ changebutton_vbox.pack_start(ignore_button, false, false, 0);
+ changebutton_vbox.pack_start(add_button, false, false, 0);
+
+ suggestion_hbox.pack_start (scrolled_window, true, true, 4);
+ suggestion_hbox.pack_end (changebutton_vbox, false, false, 0);
+
+ stop_button.set_tooltip_text(_("Stop the check"));
+ start_button.set_tooltip_text(_("Start the check"));
+
+ actionbutton_hbox.set_layout(Gtk::BUTTONBOX_END);
+ actionbutton_hbox.set_spacing(4);
+ actionbutton_hbox.add(stop_button);
+ actionbutton_hbox.add(start_button);
+
+ /*
+ * Main dialog
+ */
+ Gtk::Box *contents = _getContents();
+ contents->set_spacing(6);
+ contents->pack_start (banner_hbox, false, false, 0);
+ contents->pack_start (suggestion_hbox, true, true, 0);
+ contents->pack_start (dictionary_hbox, false, false, 0);
+ contents->pack_start (action_sep, false, false, 6);
+ contents->pack_start (actionbutton_hbox, false, false, 0);
+
+ /*
+ * Signal handlers
+ */
+ accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept));
+ ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce));
+ ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore));
+ add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd));
+ start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart));
+ stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop));
+ tree_view.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange));
+ dictionary_combo.signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged));
+ pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog));
+ desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &SpellCheck::setTargetDesktop) );
+ deskTrack.connect(GTK_WIDGET(gobj()));
+
+ show_all_children ();
+
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+}
+
+SpellCheck::~SpellCheck()
+{
+ clearRects();
+ disconnect();
+
+ desktopChangeConn.disconnect();
+ deskTrack.disconnect();
+}
+
+void SpellCheck::setDesktop(SPDesktop *desktop)
+{
+ Panel::setDesktop(desktop);
+ deskTrack.setBase(desktop);
+}
+
+void SpellCheck::setTargetDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+ this->desktop = desktop;
+ if (_working) {
+ // Stop and start on the new desktop
+ finished();
+ onStart();
+ }
+ }
+}
+
+void SpellCheck::clearRects()
+{
+ for(auto t : _rects) {
+ sp_canvas_item_hide(t);
+ sp_canvas_item_destroy(t);
+ }
+ _rects.clear();
+}
+
+void SpellCheck::disconnect()
+{
+ if (_release_connection) {
+ _release_connection.disconnect();
+ }
+ if (_modified_connection) {
+ _modified_connection.disconnect();
+ }
+}
+
+void SpellCheck::allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked)
+{
+ if (!desktop)
+ return; // no desktop to check
+
+ if (SP_IS_DEFS(r))
+ return; // we're not interested in items in defs
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return; // we're not interested in metadata
+ }
+
+ for (auto& child: r->children) {
+ if (SP_IS_ITEM (&child) && !child.cloned && !desktop->isLayer(SP_ITEM(&child))) {
+ if ((hidden || !desktop->itemIsHidden(SP_ITEM(&child))) && (locked || !SP_ITEM(&child)->isLocked())) {
+ if (SP_IS_TEXT(&child) || SP_IS_FLOWTEXT(&child))
+ l.push_back(static_cast<SPItem*>(&child));
+ }
+ }
+ allTextItems (&child, l, hidden, locked);
+ }
+ return;
+}
+
+bool
+SpellCheck::textIsValid (SPObject *root, SPItem *text)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ return (std::find(l.begin(), l.end(), text) != l.end());
+}
+
+bool SpellCheck::compareTextBboxes (gconstpointer a, gconstpointer b)//returns a<b
+{
+ SPItem *i1 = SP_ITEM(a);
+ SPItem *i2 = SP_ITEM(b);
+
+ Geom::OptRect bbox1 = i1->documentVisualBounds();
+ Geom::OptRect bbox2 = i2->documentVisualBounds();
+ if (!bbox1 || !bbox2) {
+ return false;
+ }
+
+ // vector between top left corners
+ Geom::Point diff = bbox1->min() - bbox2->min();
+
+ return diff[Geom::Y] == 0 ? (diff[Geom::X] < 0) : (diff[Geom::Y] < 0);
+}
+
+// We regenerate and resort the list every time, because user could have changed it while the
+// dialog was waiting
+SPItem *SpellCheck::getText (SPObject *root)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ std::sort(l.begin(),l.end(),SpellCheck::compareTextBboxes);
+
+ for (auto item:l) {
+ if(_seen_objects.insert(item).second)
+ return item;
+ }
+ return nullptr;
+}
+
+void
+SpellCheck::nextText()
+{
+ disconnect();
+
+ _text = getText(_root);
+ if (_text) {
+
+ _modified_connection = (SP_OBJECT(_text))->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified));
+ _release_connection = (SP_OBJECT(_text))->connectRelease(sigc::mem_fun(*this, &SpellCheck::onObjReleased));
+
+ _layout = te_get_layout (_text);
+ _begin_w = _layout->begin();
+ }
+ _end_w = _begin_w;
+ _word.clear();
+}
+
+void SpellCheck::deleteSpeller() {
+#if HAVE_ASPELL
+ if (_speller) {
+ aspell_speller_save_all_word_lists(_speller);
+ delete_aspell_speller(_speller);
+ _speller = nullptr;
+ }
+#endif
+}
+
+bool SpellCheck::updateSpeller() {
+#if HAVE_ASPELL
+ deleteSpeller();
+
+ auto lang = dictionary_combo.get_active_text();
+ if (!lang.empty()) {
+ AspellConfig *config = new_aspell_config();
+ aspell_config_replace(config, "lang", lang.c_str());
+ aspell_config_replace(config, "encoding", "UTF-8");
+ AspellCanHaveError *ret = new_aspell_speller(config);
+ delete_aspell_config(config);
+ if (aspell_error(ret) != nullptr) {
+ banner_label.set_text(aspell_error_message(ret));
+ delete_aspell_can_have_error(ret);
+ } else {
+ _speller = to_aspell_speller(ret);
+ }
+ }
+
+ return _speller != nullptr;
+#else
+ return false;
+#endif
+}
+
+bool
+SpellCheck::init(SPDesktop *d)
+{
+ desktop = d;
+
+ start_button.set_sensitive(false);
+
+ _stops = 0;
+ _adds = 0;
+ clearRects();
+
+ if (!updateSpeller())
+ return false;
+
+ _root = desktop->getDocument()->getRoot();
+
+ // empty the list of objects we've checked
+ _seen_objects.clear();
+
+ // grab first text
+ nextText();
+
+ _working = true;
+
+ return true;
+}
+
+void
+SpellCheck::finished ()
+{
+ deleteSpeller();
+
+ clearRects();
+ disconnect();
+
+ //desktop->clearWaitingCursor();
+
+ tree_view.unset_model();
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+ start_button.set_sensitive(true);
+
+ {
+ gchar *label;
+ if (_stops)
+ label = g_strdup_printf(_("<b>Finished</b>, <b>%d</b> words added to dictionary"), _adds);
+ else
+ label = g_strdup_printf("%s", _("<b>Finished</b>, nothing suspicious found"));
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ _seen_objects.clear();
+
+ desktop = nullptr;
+ _root = nullptr;
+
+ _working = false;
+}
+
+bool
+SpellCheck::nextWord()
+{
+ if (!_working)
+ return false;
+
+ if (!_text) {
+ finished();
+ return false;
+ }
+ _word.clear();
+
+ while (_word.size() == 0) {
+ _begin_w = _end_w;
+
+ if (!_layout || _begin_w == _layout->end()) {
+ nextText();
+ return false;
+ }
+
+ if (!_layout->isStartOfWord(_begin_w)) {
+ _begin_w.nextStartOfWord();
+ }
+
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+
+ // try to link this word with the next if separated by '
+ SPObject *char_item = nullptr;
+ Glib::ustring::iterator text_iter;
+ _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (this_char == '\'' || this_char == 0x2019) {
+ Inkscape::Text::Layout::iterator end_t = _end_w;
+ end_t.nextCharacter();
+ _layout->getSourceOfCharacter(end_t, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (g_ascii_isalpha(this_char)) { // 's
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+ }
+ }
+ }
+
+ // skip words containing digits
+ if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) {
+ bool digits = false;
+ for (unsigned int i : _word) {
+ if (g_unichar_isdigit(i)) {
+ digits = true;
+ break;
+ }
+ }
+ if (digits) {
+ return false;
+ }
+ }
+
+ // skip ALL-CAPS words
+ if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) {
+ bool allcaps = true;
+ for (unsigned int i : _word) {
+ if (!g_unichar_isupper(i)) {
+ allcaps = false;
+ break;
+ }
+ }
+ if (allcaps) {
+ return false;
+ }
+ }
+
+ int have = 0;
+
+#if HAVE_ASPELL
+ // run it by all active spellers
+ if (_speller) {
+ have += aspell_speller_check(_speller, _word.c_str(), -1);
+ }
+#endif /* HAVE_ASPELL */
+
+ if (have == 0) { // not found in any!
+ _stops ++;
+
+ //desktop->clearWaitingCursor();
+
+ // display it in window
+ {
+ gchar *label = g_strdup_printf(_("Not in dictionary: <b>%s</b>"), _word.c_str());
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ tree_view.set_sensitive(true);
+ ignore_button.set_sensitive(true);
+ ignoreonce_button.set_sensitive(true);
+ add_button.set_sensitive(true);
+ stop_button.set_sensitive(true);
+
+ // draw rect
+ std::vector<Geom::Point> points =
+ _layout->createSelectionShape(_begin_w, _end_w, _text->i2dt_affine());
+ if (points.size() >= 4) { // we may not have a single quad if this is a clipped part of text on path; in that case skip drawing the rect
+ Geom::Point tl, br;
+ tl = br = points.front();
+ for (auto & point : points) {
+ if (point[Geom::X] < tl[Geom::X])
+ tl[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] < tl[Geom::Y])
+ tl[Geom::Y] = point[Geom::Y];
+ if (point[Geom::X] > br[Geom::X])
+ br[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] > br[Geom::Y])
+ br[Geom::Y] = point[Geom::Y];
+ }
+
+ // expand slightly
+ Geom::Rect area = Geom::Rect(tl, br);
+ double mindim = fabs(tl[Geom::Y] - br[Geom::Y]);
+ if (fabs(tl[Geom::X] - br[Geom::X]) < mindim)
+ mindim = fabs(tl[Geom::X] - br[Geom::X]);
+ area.expandBy(MAX(0.05 * mindim, 1));
+
+ // create canvas path rectangle, red stroke
+ SPCanvasItem *rect = sp_canvas_bpath_new(desktop->getSketch(), nullptr);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(rect), 0xff0000ff, 3.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(rect), 0, SP_WIND_RULE_NONZERO);
+ SPCurve *curve = new SPCurve();
+ curve->moveto(area.corner(0));
+ curve->lineto(area.corner(1));
+ curve->lineto(area.corner(2));
+ curve->lineto(area.corner(3));
+ curve->lineto(area.corner(0));
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(rect), curve);
+ sp_canvas_item_show(rect);
+ _rects.push_back(rect);
+
+ // scroll to make it all visible
+ Geom::Point const center = desktop->get_display_area().midpoint();
+ area.expandBy(0.5 * mindim);
+ Geom::Point scrollto;
+ double dist = 0;
+ for (unsigned corner = 0; corner < 4; corner ++) {
+ if (Geom::L2(area.corner(corner) - center) > dist) {
+ dist = Geom::L2(area.corner(corner) - center);
+ scrollto = area.corner(corner);
+ }
+ }
+ desktop->scroll_to_point (scrollto, 1.0);
+ }
+
+ // select text; if in Text tool, position cursor to the beginning of word
+ // unless it is already in the word
+ if (desktop->selection->singleItem() != _text)
+ desktop->selection->set (_text);
+ if (tools_isactive(desktop, TOOLS_TEXT)) {
+ Inkscape::Text::Layout::iterator *cursor =
+ sp_text_context_get_cursor_position(SP_TEXT_CONTEXT(desktop->event_context), _text);
+ if (!cursor) // some other text is selected there
+ desktop->selection->set (_text);
+ else if (*cursor <= _begin_w || *cursor >= _end_w)
+ sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w);
+ }
+
+#if HAVE_ASPELL
+
+ // get suggestions
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ unsigned n_sugg = 0;
+
+ if (_speller) {
+ const AspellWordList *wl = aspell_speller_suggest(_speller, _word.c_str(), -1);
+ AspellStringEnumeration * els = aspell_word_list_elements(wl);
+ const char *sugg;
+ Gtk::TreeModel::iterator iter;
+
+ while ((sugg = aspell_string_enumeration_next(els)) != nullptr) {
+ iter = model->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[tree_columns.suggestions] = sugg;
+
+ // select first suggestion
+ if (++n_sugg == 1) {
+ tree_view.get_selection()->select(iter);
+ }
+ }
+ delete_aspell_string_enumeration(els);
+ }
+
+ accept_button.set_sensitive(n_sugg > 0);
+
+#endif /* HAVE_ASPELL */
+
+ return true;
+
+ }
+ return false;
+}
+
+
+
+void
+SpellCheck::deleteLastRect ()
+{
+ if (!_rects.empty()) {
+ sp_canvas_item_hide(_rects.back());
+ sp_canvas_item_destroy(_rects.back());
+ _rects.pop_back(); // pop latest-prepended rect
+ }
+}
+
+void SpellCheck::doSpellcheck ()
+{
+ if (_langs.empty()) {
+ return;
+ }
+
+ banner_label.set_markup(_("<i>Checking...</i>"));
+
+ //desktop->setWaitingCursor();
+
+ while (_working)
+ if (nextWord())
+ break;
+}
+
+void SpellCheck::onTreeSelectionChange()
+{
+ accept_button.set_sensitive(true);
+}
+
+void SpellCheck::onObjModified (SPObject* /* blah */, unsigned int /* bleh */)
+{
+ if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it
+ _local_change = false;
+ return;
+ }
+
+ if (_working && _root) {
+ // user may have edited the text we're checking; try to do the most sensible thing in this
+ // situation
+
+ // just in case, re-get text's layout
+ _layout = te_get_layout (_text);
+
+ // re-get the word
+ _layout->validateIterator(&_begin_w);
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ Glib::ustring word_new = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ if (word_new != _word) {
+ _end_w = _begin_w;
+ deleteLastRect ();
+ doSpellcheck (); // recheck this word and go ahead if it's ok
+ }
+ }
+}
+
+void SpellCheck::onObjReleased (SPObject* /* blah */)
+{
+ if (_working && _root) {
+ // the text object was deleted
+ deleteLastRect ();
+ nextText();
+ doSpellcheck (); // get next text and continue
+ }
+}
+
+void SpellCheck::onAccept ()
+{
+ // insert chosen suggestion
+
+ Glib::RefPtr<Gtk::TreeSelection> selection = tree_view.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring sugg = row[tree_columns.suggestions];
+
+ if (sugg.length() > 0) {
+ //g_print("chosen: %s\n", sugg);
+ _local_change = true;
+ sp_te_replace(_text, _begin_w, _end_w, sugg.c_str());
+ // find the end of the word anew
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Fix spelling"));
+ }
+ }
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnore ()
+{
+#if HAVE_ASPELL
+ if (_speller) {
+ aspell_speller_add_to_session(_speller, _word.c_str(), -1);
+ }
+#endif /* HAVE_ASPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnoreOnce ()
+{
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onAdd ()
+{
+ _adds++;
+
+#if HAVE_ASPELL
+ if (_speller) {
+ aspell_speller_add_to_personal(_speller, _word.c_str(), -1);
+ }
+#endif /* HAVE_ASPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onStop ()
+{
+ finished();
+}
+
+void
+SpellCheck::onStart ()
+{
+ if (init (SP_ACTIVE_DESKTOP))
+ doSpellcheck();
+}
+
+void SpellCheck::onLanguageChanged()
+{
+ if (!_working) {
+ onStart();
+ return;
+ }
+
+ if (!updateSpeller()) {
+ return;
+ }
+
+ // recheck current word
+ _end_w = _begin_w;
+ deleteLastRect();
+ doSpellcheck();
+}
+}
+}
+}
+
+/*
+ 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/spellcheck.h b/src/ui/dialog/spellcheck.h
new file mode 100644
index 0000000..39d1285
--- /dev/null
+++ b/src/ui/dialog/spellcheck.h
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Spellcheck dialog
+ */
+/* Authors:
+ * bulia byak <bulia@users.sf.net>
+ *
+ * Copyright (C) 2009 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_SPELLCHECK_H
+#define SEEN_SPELLCHECK_H
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <vector>
+#include <set>
+
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/separator.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+
+#include "ui/dialog/desktop-tracker.h"
+#include "ui/widget/panel.h"
+
+#include "text-editing.h"
+
+#if HAVE_ASPELL
+#include <aspell.h>
+#endif /* HAVE_ASPELL */
+
+class SPDesktop;
+class SPObject;
+class SPItem;
+class SPCanvasItem;
+
+namespace Inkscape {
+class Preferences;
+
+namespace UI {
+namespace Dialog {
+
+/**
+ *
+ * A dialog widget to checking spelling of text elements in the document
+ * Uses ASpell and one of the languages set in the users preference file
+ *
+ */
+class SpellCheck : public Widget::Panel {
+public:
+ SpellCheck ();
+ ~SpellCheck () override;
+
+ static SpellCheck &getInstance() { return *new SpellCheck(); }
+
+ static std::vector<std::string> get_available_langs();
+
+private:
+
+ /**
+ * Remove the highlight rectangle form the canvas
+ */
+ void clearRects();
+
+ /**
+ * Release handlers to the selected item
+ */
+ void disconnect();
+
+ /**
+ * Returns a list of all the text items in the SPObject
+ */
+ void allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked);
+
+ /**
+ * Is text inside the SPOject's tree
+ */
+ bool textIsValid (SPObject *root, SPItem *text);
+
+ /**
+ * Compare the visual bounds of 2 SPItems referred to by a and b
+ */
+ static bool compareTextBboxes (gconstpointer a, gconstpointer b);
+ SPItem *getText (SPObject *root);
+ void nextText ();
+
+ /**
+ * Initialize the controls and aspell
+ */
+ bool init (SPDesktop *desktop);
+
+ /**
+ * Cleanup after spellcheck is finished
+ */
+ void finished ();
+
+ /**
+ * Find the next word to spell check
+ */
+ bool nextWord();
+ void deleteLastRect ();
+ void doSpellcheck ();
+
+ /**
+ * Update speller from language combobox
+ * @return true if update was successful
+ */
+ bool updateSpeller();
+ void deleteSpeller();
+
+ /**
+ * Accept button clicked
+ */
+ void onAccept ();
+
+ /**
+ * Ignore button clicked
+ */
+ void onIgnore ();
+
+ /**
+ * Ignore once button clicked
+ */
+ void onIgnoreOnce ();
+
+ /**
+ * Add button clicked
+ */
+ void onAdd ();
+
+ /**
+ * Stop button clicked
+ */
+ void onStop ();
+
+ /**
+ * Start button clicked
+ */
+ void onStart ();
+
+ /**
+ * Language selection changed
+ */
+ void onLanguageChanged();
+
+ /**
+ * Selected object modified on canvas
+ */
+ void onObjModified (SPObject* /* blah */, unsigned int /* bleh */);
+
+ /**
+ * Selected object removed from canvas
+ */
+ void onObjReleased (SPObject* /* blah */);
+
+ /**
+ * Selection in suggestions text view changed
+ */
+ void onTreeSelectionChange();
+
+ /**
+ * 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);
+
+ SPObject *_root;
+
+#if HAVE_ASPELL
+ AspellSpeller *_speller = nullptr;
+#endif /* HAVE_ASPELL */
+
+ /**
+ * list of canvasitems (currently just rects) that mark misspelled things on canvas
+ */
+ std::vector<SPCanvasItem *> _rects;
+
+ /**
+ * list of text objects we have already checked in this session
+ */
+ std::set<SPItem *> _seen_objects;
+
+ /**
+ * the object currently being checked
+ */
+ SPItem *_text;
+
+ /**
+ * current objects layout
+ */
+ Inkscape::Text::Layout const *_layout;
+
+ /**
+ * iterators for the start and end of the current word
+ */
+ Inkscape::Text::Layout::iterator _begin_w;
+ Inkscape::Text::Layout::iterator _end_w;
+
+ /**
+ * the word we're checking
+ */
+ Glib::ustring _word;
+
+ /**
+ * counters for the number of stops and dictionary adds
+ */
+ int _stops;
+ int _adds;
+
+ /**
+ * true if we are in the middle of a check
+ */
+ bool _working;
+
+ /**
+ * connect to the object being checked in case it is modified or deleted by user
+ */
+ sigc::connection _modified_connection;
+ sigc::connection _release_connection;
+
+ /**
+ * true if the spell checker dialog has changed text, to suppress modified callback
+ */
+ bool _local_change;
+
+ Inkscape::Preferences *_prefs;
+
+ std::vector<std::string> _langs;
+
+ /*
+ * Dialogs widgets
+ */
+ Gtk::Label banner_label;
+ Gtk::ButtonBox banner_hbox;
+ Gtk::ScrolledWindow scrolled_window;
+ Gtk::TreeView tree_view;
+ Glib::RefPtr<Gtk::ListStore> model;
+
+ Gtk::HBox suggestion_hbox;
+ Gtk::VBox changebutton_vbox;
+ Gtk::Button accept_button;
+ Gtk::Button ignoreonce_button;
+ Gtk::Button ignore_button;
+
+ Gtk::Button add_button;
+ Gtk::Button pref_button;
+ Gtk::Label dictionary_label;
+ Gtk::ComboBoxText dictionary_combo;
+ Gtk::HBox dictionary_hbox;
+ Gtk::Separator action_sep;
+ Gtk::Button stop_button;
+ Gtk::Button start_button;
+ Gtk::ButtonBox actionbutton_hbox;
+
+ SPDesktop * desktop;
+ DesktopTracker deskTrack;
+ sigc::connection desktopChangeConn;
+
+ class TreeColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ TreeColumns()
+ {
+ add(suggestions);
+ }
+ ~TreeColumns() override = default;
+ Gtk::TreeModelColumn<Glib::ustring> suggestions;
+ };
+ TreeColumns tree_columns;
+
+};
+
+}
+}
+}
+
+#endif /* !SEEN_SPELLCHECK_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/dialog/styledialog.cpp b/src/ui/dialog/styledialog.cpp
new file mode 100644
index 0000000..872b3bf
--- /dev/null
+++ b/src/ui/dialog/styledialog.cpp
@@ -0,0 +1,1690 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for CSS styles
+ */
+/* Authors:
+ * Kamalpreet Kaur Grewal
+ * Tavmjong Bah
+ * Jabiertxof
+ *
+ * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "styledialog.h"
+#include "attribute-rel-svg.h"
+#include "attributes.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "selection.h"
+#include "style-internal.h"
+#include "style.h"
+#include "svg/svg-color.h"
+#include "ui/icon-loader.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 <map>
+#include <regex>
+#include <utility>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape
+// #define DEBUG_STYLEDIALOG
+// #define G_LOG_DOMAIN "STYLEDIALOG"
+
+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(' ')); \
+ x.erase(x.find_last_not_of(' ') + 1);
+
+namespace Inkscape {
+
+/**
+ * Get the first <style> element's first text node. If no such node exists and
+ * `create_if_missing` is false, then return NULL.
+ *
+ * Only finds <style> elements in root or in root-level <defs>.
+ */
+XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing)
+{
+ static GQuark const CODE_svg_style = g_quark_from_static_string("svg:style");
+ static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs");
+
+ XML::Node *styleNode = nullptr;
+ XML::Node *textNode = nullptr;
+
+ for (auto *node = root->firstChild(); node; node = node->next()) {
+ if (node->code() == CODE_svg_defs) {
+ textNode = get_first_style_text_node(node, false);
+ if (textNode != nullptr) {
+ return textNode;
+ }
+ }
+
+ if (node->code() == CODE_svg_style) {
+ styleNode = node;
+ break;
+ }
+ }
+
+ if (styleNode == nullptr) {
+ if (!create_if_missing)
+ return nullptr;
+
+ styleNode = root->document()->createElement("svg:style");
+ root->addChild(styleNode, nullptr);
+ Inkscape::GC::release(styleNode);
+ }
+
+ for (auto *node = styleNode->firstChild(); node; node = node->next()) {
+ if (node->type() == XML::TEXT_NODE) {
+ textNode = node;
+ break;
+ }
+ }
+
+ if (textNode == nullptr) {
+ if (!create_if_missing)
+ return nullptr;
+
+ textNode = root->document()->createTextNode("");
+ styleNode->appendChild(textNode);
+ Inkscape::GC::release(textNode);
+ }
+
+ return textNode;
+}
+
+namespace UI {
+namespace Dialog {
+
+// Keeps a watch on style element
+class StyleDialog::NodeObserver : public Inkscape::XML::NodeObserver {
+ public:
+ NodeObserver(StyleDialog *styledialog)
+ : _styledialog(styledialog)
+ {
+ g_debug("StyleDialog::NodeObserver: Constructor");
+ };
+
+ void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared old_content,
+ Inkscape::Util::ptr_shared new_content) override;
+
+ StyleDialog *_styledialog;
+};
+
+
+void StyleDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/,
+ Inkscape::Util::ptr_shared /*old_content*/,
+ Inkscape::Util::ptr_shared /*new_content*/)
+{
+
+ g_debug("StyleDialog::NodeObserver::notifyContentChanged");
+ _styledialog->_updating = false;
+ _styledialog->readStyleElement();
+}
+
+
+// Keeps a watch for new/removed/changed nodes
+// (Must update objects that selectors match.)
+class StyleDialog::NodeWatcher : public Inkscape::XML::NodeObserver {
+ public:
+ NodeWatcher(StyleDialog *styledialog)
+ : _styledialog(styledialog)
+ {
+ g_debug("StyleDialog::NodeWatcher: Constructor");
+ };
+
+ void notifyChildAdded(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child,
+ Inkscape::XML::Node * /*prev*/) override
+ {
+ _styledialog->_nodeAdded(child);
+ }
+
+ void notifyChildRemoved(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child,
+ Inkscape::XML::Node * /*prev*/) override
+ {
+ _styledialog->_nodeRemoved(child);
+ }
+ /* void notifyContentChanged(Inkscape::XML::Node &node,
+ Inkscape::Util::ptr_shared old_content,
+ Inkscape::Util::ptr_shared new_content) override{
+ if ( _styledialog && _repr && _textNode == node) {
+ _styledialog->_stylesheetChanged( node );
+ }
+ };
+ */
+ 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");
+ static GQuark const CODE_style = g_quark_from_static_string("style");
+
+ if (qname == CODE_id || qname == CODE_class || qname == CODE_style) {
+ _styledialog->_nodeChanged(node);
+ }
+ }
+
+ StyleDialog *_styledialog;
+};
+
+void StyleDialog::_nodeAdded(Inkscape::XML::Node &node)
+{
+ readStyleElement();
+}
+
+void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr)
+{
+ if (_textNode == &repr) {
+ _textNode = nullptr;
+ }
+
+ readStyleElement();
+}
+
+void StyleDialog::_nodeChanged(Inkscape::XML::Node &object)
+{
+ g_debug("StyleDialog::_nodeChanged");
+ readStyleElement();
+}
+
+/* void
+StyleDialog::_stylesheetChanged( Inkscape::XML::Node &repr ) {
+ std::cout << "Style tag modified" << std::endl;
+ readStyleElement();
+} */
+
+/**
+ * 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.
+ */
+StyleDialog::StyleDialog()
+ : UI::Widget::Panel("/dialogs/style", SP_VERB_DIALOG_STYLE)
+ , _updating(false)
+ , _textNode(nullptr)
+ , _scroolpos(0)
+ , _desktopTracker()
+ , _deleted_pos(0)
+ , _deletion(false)
+{
+ g_debug("StyleDialog::StyleDialog");
+
+ m_nodewatcher.reset(new StyleDialog::NodeWatcher(this));
+ m_styletextwatcher.reset(new StyleDialog::NodeObserver(this));
+
+ // Pack widgets
+ _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET);
+ _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _styleBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ _styleBox.set_valign(Gtk::ALIGN_START);
+ _scrolledWindow.add(_styleBox);
+ Gtk::Label *infotoggler = Gtk::manage(new Gtk::Label(_("Edit Full Stylesheet")));
+ infotoggler->get_style_context()->add_class("inksmall");
+ _vadj = _scrolledWindow.get_vadjustment();
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscrool));
+ _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET);
+ // Document & Desktop
+ _desktop_changed_connection =
+ _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &StyleDialog::_handleDesktopChanged));
+ _desktopTracker.connect(GTK_WIDGET(gobj()));
+
+ _document_replaced_connection =
+ getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced));
+
+ _selection_changed_connection = getDesktop()->getSelection()->connectChanged(
+ sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged)));
+
+ // Add watchers
+ _updateWatchers(getDesktop());
+
+ // Load tree
+ readStyleElement();
+}
+
+void StyleDialog::_vscrool()
+{
+ if (!_scroollock) {
+ _scroolpos = _vadj->get_value();
+ } else {
+ _vadj->set_value(_scroolpos);
+ _scroollock = false;
+ }
+}
+
+Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector)
+{
+ g_debug("SelectorsDialog::fixCSSSelectors");
+ REMOVE_SPACES(selector);
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector);
+ Glib::ustring my_selector = selector + " {"; // Parsing fails sometimes without '{'. Fix me
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.c_str(), CR_UTF_8);
+ for (auto token : tokens) {
+ REMOVE_SPACES(token);
+ std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token);
+ for (auto subtoken : subtokens) {
+ REMOVE_SPACES(subtoken);
+ Glib::ustring my_selector = subtoken + " {"; // Parsing fails sometimes without '{'. Fix me
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.c_str(), CR_UTF_8);
+ gchar *selectorchar = reinterpret_cast<gchar *>(cr_selector_to_string(cr_selector));
+ if (selectorchar) {
+ Glib::ustring toadd = Glib::ustring(selectorchar);
+ g_free(selectorchar);
+ if (toadd[0] != '.' && toadd[0] != '#' && toadd.size() > 1) {
+ auto i = std::min(toadd.find("#"), toadd.find("."));
+ Glib::ustring tag = toadd;
+ if (i != std::string::npos) {
+ tag = tag.substr(0, i);
+ }
+ if (!SPAttributeRelSVG::isSVGElement(tag)) {
+ if (tokens.size() == 1) {
+ tag = "." + tag;
+ return tag;
+ } else {
+ return "";
+ }
+ }
+ }
+ }
+ }
+ }
+ if (cr_selector) {
+ return selector;
+ }
+ return "";
+}
+
+/**
+ * Class destructor
+ */
+StyleDialog::~StyleDialog()
+{
+ g_debug("StyleDialog::~StyleDialog");
+ _desktop_changed_connection.disconnect();
+ _document_replaced_connection.disconnect();
+ _selection_changed_connection.disconnect();
+}
+
+void StyleDialog::_reload() { readStyleElement(); }
+
+/**
+ * @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 *StyleDialog::_getStyleTextNode(bool create_if_missing)
+{
+ g_debug("StyleDialog::_getStyleTextNoded");
+
+ 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;
+}
+
+
+Glib::RefPtr<Gtk::TreeModel> StyleDialog::_selectTree(Glib::ustring selector)
+{
+ g_debug("StyleDialog::_selectTree");
+
+ Gtk::Label *selectorlabel;
+ Glib::RefPtr<Gtk::TreeModel> model;
+ for (auto fullstyle : _styleBox.get_children()) {
+ Gtk::Box *style = dynamic_cast<Gtk::Box *>(fullstyle);
+ for (auto stylepart : style->get_children()) {
+ switch (style->child_property_position(*stylepart)) {
+ case 0: {
+ Gtk::Box *selectorbox = dynamic_cast<Gtk::Box *>(stylepart);
+ for (auto styleheader : selectorbox->get_children()) {
+ if (!selectorbox->child_property_position(*styleheader)) {
+ selectorlabel = dynamic_cast<Gtk::Label *>(styleheader);
+ }
+ }
+ break;
+ }
+ case 1: {
+ Glib::ustring wdg_selector = selectorlabel->get_text();
+ if (wdg_selector == selector) {
+ Gtk::TreeView *treeview = dynamic_cast<Gtk::TreeView *>(stylepart);
+ if (treeview) {
+ return treeview->get_model();
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+ return model;
+}
+
+void StyleDialog::setCurrentSelector(Glib::ustring current_selector)
+{
+ g_debug("StyleDialog::setCurrentSelector");
+ _current_selector = current_selector;
+ readStyleElement();
+}
+
+// copied from style.cpp:1499
+static bool is_url(char const *p)
+{
+ if (p == nullptr)
+ return false;
+ /** \todo
+ * FIXME: I'm not sure if this applies to SVG as well, but CSS2 says any URIs
+ * in property values must start with 'url('.
+ */
+ return (g_ascii_strncasecmp(p, "url(", 4) == 0);
+}
+
+/**
+ * Fill the Gtk::TreeStore from the svg:style element.
+ */
+void StyleDialog::readStyleElement()
+{
+ g_debug("StyleDialog::readStyleElement");
+
+ if (_updating)
+ return; // Don't read if we wrote style element.
+ _updating = true;
+ _scroollock = true;
+ Inkscape::XML::Node *textNode = _getStyleTextNode();
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+
+ // 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 */)
+
+ bool breakme = false;
+ size_t start = content.find("/*");
+ size_t open = content.find("{", start + 1);
+ size_t close = content.find("}", start + 1);
+ size_t end = content.find("*/", close + 1);
+ while (!breakme) {
+ if (open == std::string::npos || close == std::string::npos || end == std::string::npos) {
+ breakme = true;
+ break;
+ }
+ while (open < close) {
+ open = content.find("{", close + 1);
+ close = content.find("}", close + 1);
+ end = content.find("*/", close + 1);
+ size_t reopen = content.find("{", close + 1);
+ if (open == std::string::npos || end == std::string::npos || end < reopen) {
+ if (end < reopen) {
+ content = content.erase(start, end - start + 2);
+ } else {
+ breakme = true;
+ }
+ break;
+ }
+ }
+ start = content.find("/*", start + 1);
+ open = content.find("{", start + 1);
+ close = content.find("}", start + 1);
+ end = content.find("*/", close + 1);
+ }
+
+ // 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<Glib::Regex> regex1 =
+ // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}");
+ //
+ // Glib::MatchInfo minfo;
+ // regex1->match(content, minfo);
+
+ // Split on curly brackets. Even tokens are selectors, odd are values.
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content);
+ _owner_style.clear();
+ // If text node is empty, return (avoids problem with negative below).
+
+ for (auto child : _styleBox.get_children()) {
+ _styleBox.remove(*child);
+ delete child;
+ }
+ Inkscape::Selection *selection = getDesktop()->getSelection();
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = getDesktop()->getDocument()->getXMLDialogSelectedObject();
+ if (obj && !obj->getRepr()) {
+ obj = nullptr; // treat detached object as no selection
+ }
+ }
+
+ Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-css.glade");
+ Glib::RefPtr<Gtk::Builder> _builder;
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog");
+ return;
+ }
+ gint selectorpos = 0;
+ Gtk::Box *css_selector_container;
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ Gtk::Label *css_selector;
+ _builder->get_widget("CSSSelector", css_selector);
+ Gtk::EventBox *css_selector_event_add;
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ css_selector->set_text("element");
+ Gtk::TreeView *css_tree;
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_element");
+ Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns);
+ css_tree->set_model(store);
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "style_properties", selectorpos));
+ Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_placeholder_text() = _("value");
+ value->property_editable() = true;
+ value->signal_edited().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->set_expand(true);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ Inkscape::UI::Widget::IconRenderer *urlRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ urlRenderer->add_icon("empty-icon");
+ urlRenderer->add_icon("edit-redo");
+ int urlCol = css_tree->append_column(" ", *urlRenderer) - 1;
+ Gtk::TreeViewColumn *urlcol = css_tree->get_column(urlCol);
+ if (urlcol) {
+ urlcol->set_min_width(40);
+ urlcol->set_max_width(40);
+ urlRenderer->signal_activated().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onLinkObj), store));
+ urlcol->add_attribute(urlRenderer->property_icon(), _mColumns._colLinked);
+ }
+ std::map<Glib::ustring, Glib::ustring> attr_prop;
+ Gtk::TreeModel::Path path;
+ bool empty = true;
+ if (obj && obj->getRepr()->attribute("style")) {
+ Glib::ustring style = obj->getRepr()->attribute("style");
+ attr_prop = parseStyle(style);
+ for (auto iter : obj->style->properties()) {
+ if (attr_prop.count(iter->name())) {
+ empty = false;
+ Gtk::TreeModel::Row row = *(store->prepend());
+ row[_mColumns._colSelector] = "style_properties";
+ row[_mColumns._colSelectorPos] = 0;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = iter->name();
+ row[_mColumns._colValue] = iter->get_value();
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ row[_mColumns._colHref] = nullptr;
+ row[_mColumns._colLinked] = false;
+ if (is_url(iter->get_value().c_str())) {
+ Glib::ustring id = iter->get_value();
+ id = id.substr(5, id.size() - 6);
+ SPObject *elemref = nullptr;
+ if ((elemref = document->getObjectById(id.c_str()))) {
+ row[_mColumns._colHref] = elemref;
+ row[_mColumns._colLinked] = true;
+ }
+ }
+ _addOwnerStyle(iter->name(), "style attribute");
+ }
+ }
+ // this is to fix a bug on cairo win:
+ // https://gitlab.freedesktop.org/cairo/cairo/issues/338
+ // TODO: check if inkscape min cairo version has applied the patch proposed and remove (3 times)
+ if (empty) {
+ css_tree->hide();
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ }
+ selectorpos++;
+ if (tokens.size() == 0) {
+ _updating = false;
+ return;
+ }
+ for (unsigned i = 0; i < tokens.size() - 1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ REMOVE_SPACES(selector); // Remove leading/trailing spaces
+ // Get list of objects selector matches
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ Glib::ustring selector_orig = selector;
+ if (!selectordata.empty()) {
+ selector = selectordata.back();
+ }
+ std::vector<SPObject *> objVec = _getObjVec(selector);
+ if (obj) {
+ bool stop = true;
+ for (auto objel : objVec) {
+ if (objel == obj) {
+ stop = false;
+ }
+ }
+ if (stop) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ }
+ if (!obj && _current_selector != "" && _current_selector != selector) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ if (!obj) {
+ bool present = false;
+ for (auto objv : objVec) {
+ for (auto objsel : selection->objects()) {
+ if (objv == objsel) {
+ present = true;
+ break;
+ }
+ }
+ if (present) {
+ break;
+ }
+ }
+ if (!present) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ }
+ 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 << "StyleDialog::readStyleElement: Missing values "
+ "for last selector!"
+ << std::endl;
+ }
+ Glib::RefPtr<Gtk::Builder> _builder;
+ 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::Box *css_selector_container;
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ Gtk::Label *css_selector;
+ _builder->get_widget("CSSSelector", css_selector);
+ Gtk::EventBox *css_selector_event_box;
+ _builder->get_widget("CSSSelectorEventBox", css_selector_event_box);
+ Gtk::Entry *css_edit_selector;
+ _builder->get_widget("CSSEditSelector", css_edit_selector);
+ Gtk::EventBox *css_selector_event_add;
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ css_selector->set_text(selector);
+ Gtk::TreeView *css_tree;
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_sheet");
+ Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns);
+ css_tree->set_model(store);
+ // I comment this feature, is working but seems obscure to undertand
+ // the user can edit selector name in current implementation
+ /* css_selector_event_box->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorStartEdit), css_selector, css_edit_selector));
+ css_edit_selector->signal_key_press_event().connect(sigc::bind(
+ sigc::mem_fun(*this, &StyleDialog::_selectorEditKeyPress), store, css_selector, css_edit_selector));
+ css_edit_selector->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorActivate), store, css_selector, css_edit_selector));
+ */
+ Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererToggle *isactive = Gtk::manage(new Gtk::CellRendererToggle());
+ isactive->property_activatable() = true;
+ addCol = css_tree->append_column(" ", *isactive) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(isactive->property_active(), _mColumns._colActive);
+ isactive->signal_toggled().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_activeToggled), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_editable() = true;
+ value->property_placeholder_text() = _("value");
+ value->signal_edited().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ Glib::ustring style = properties;
+ Glib::ustring comments = "";
+ while (style.find("/*") != std::string::npos) {
+ size_t beg = style.find("/*");
+ size_t end = style.find("*/");
+ if (end != std::string::npos && beg != std::string::npos) {
+ comments = comments.append(style, beg + 2, end - beg - 2);
+ style = style.erase(beg, end - beg + 2);
+ }
+ }
+ std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet = parseStyle(style);
+ std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet_comments = parseStyle(comments);
+ std::map<Glib::ustring, std::pair<Glib::ustring, bool>> result_props;
+ for (auto styled : attr_prop_styleshet) {
+ result_props[styled.first] = std::make_pair(styled.second, true);
+ }
+ for (auto styled : attr_prop_styleshet_comments) {
+ result_props[styled.first] = std::make_pair(styled.second, false);
+ }
+ empty = true;
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, selector_orig, selectorpos));
+ if (obj) {
+ for (auto iter : result_props) {
+ empty = false;
+ Gtk::TreeIter iterstore = store->append();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore;
+ Gtk::TreeModel::Row row = *(iterstore);
+ row[_mColumns._colSelector] = selector_orig;
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = iter.second.second;
+ row[_mColumns._colName] = iter.first;
+ row[_mColumns._colValue] = iter.second.first;
+ const Glib::ustring value = row[_mColumns._colValue];
+ if (iter.second.second) {
+ Glib::ustring val = "";
+ for (auto iterprop : obj->style->properties()) {
+ if (iterprop->style_src != SP_STYLE_SRC_UNSET && iterprop->name() == iter.first) {
+ val = iterprop->get_value();
+ break;
+ }
+ }
+ guint32 r1 = 0; // if there's no color, return black
+ r1 = sp_svg_read_color(value.c_str(), r1);
+ guint32 r2 = 0; // if there's no color, return black
+ r2 = sp_svg_read_color(val.c_str(), r2);
+ if (attr_prop.count(iter.first) || (value != val && (r1 == 0 || r1 != r2))) {
+ row[_mColumns._colStrike] = true;
+ row[_mColumns._colOwner] = Glib::ustring("");
+ } else {
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ _addOwnerStyle(iter.first, selector);
+ }
+ } else {
+ row[_mColumns._colStrike] = true;
+ Glib::ustring tooltiptext = _("This value is commented");
+ row[_mColumns._colOwner] = tooltiptext;
+ }
+ }
+ } else {
+ for (auto iter : result_props) {
+ empty = false;
+ Gtk::TreeModel::Row row = *(store->prepend());
+ row[_mColumns._colSelector] = selector_orig;
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = iter.second.second;
+ row[_mColumns._colName] = iter.first;
+ row[_mColumns._colValue] = iter.second.first;
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Stylesheet value");
+ }
+ }
+ if (empty) {
+ css_tree->hide();
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ selectorpos++;
+ }
+ 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("CSSSelector", css_selector);
+ css_selector->set_text("element.attributes");
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ store = Gtk::TreeStore::create(_mColumns);
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_attribute");
+ css_tree->set_model(store);
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "attributes", selectorpos));
+ bool hasattributes = false;
+ empty = true;
+ if (obj) {
+ for (auto iter : obj->style->properties()) {
+ if (iter->style_src != SP_STYLE_SRC_UNSET) {
+ auto key = iter->id();
+ if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) {
+ const gchar *attr = obj->getRepr()->attribute(iter->name().c_str());
+ if (attr) {
+ if (!hasattributes) {
+ Inkscape::UI::Widget::IconRenderer *addRenderer =
+ manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_placeholder_text() = _("value");
+ value->property_editable() = true;
+ value->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ }
+ empty = false;
+ Gtk::TreeIter iterstore = store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore;
+ Gtk::TreeModel::Row row = *(iterstore);
+ row[_mColumns._colSelector] = "attributes";
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = iter->name();
+ row[_mColumns._colValue] = attr;
+ if (_owner_style.find(iter->name()) != _owner_style.end()) {
+ row[_mColumns._colStrike] = true;
+ Glib::ustring tooltiptext = Glib::ustring("");
+ row[_mColumns._colOwner] = tooltiptext;
+ } else {
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ _addOwnerStyle(iter->name(), "inline attributes");
+ }
+ hasattributes = true;
+ }
+ }
+ }
+ }
+ if (empty) {
+ css_tree->hide();
+ }
+ if (!hasattributes) {
+ for (auto widg : css_selector_container->get_children()) {
+ delete widg;
+ }
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ }
+ for (auto selector : _styleBox.get_children()) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(&selector[0]);
+ if (box) {
+ std::vector<Gtk::Widget *> childs = box->get_children();
+ if (childs.size() > 1) {
+ Gtk::TreeView *css_tree = dynamic_cast<Gtk::TreeView *>(childs[1]);
+ if (css_tree) {
+ Glib::RefPtr<Gtk::TreeModel> model = css_tree->get_model();
+ if (model) {
+ model->foreach_iter(sigc::mem_fun(*this, &StyleDialog::_on_foreach_iter));
+ }
+ }
+ }
+ }
+ }
+ if (obj) {
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ _mainBox.show_all_children();
+ _updating = false;
+}
+
+bool StyleDialog::_selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit)
+{
+ g_debug("StyleDialog::_selectorStartEdit");
+ if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
+ selector->hide();
+ selector_edit->set_text(selector->get_text());
+ selector_edit->show();
+ }
+ return false;
+}
+
+/* void StyleDialog::_selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry
+*selector_edit)
+{
+ g_debug("StyleDialog::_selectorEditKeyPress");
+ Glib::ustring newselector = fixCSSSelectors(selector_edit->get_text());
+ if (newselector.empty()) {
+ selector_edit->get_style_context()->add_class("system_error_color");
+ return;
+ }
+ _writeStyleElement(store, selector->get_text(), selector_edit->get_text());
+} */
+
+bool StyleDialog::_selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector,
+ Gtk::Entry *selector_edit)
+{
+ g_debug("StyleDialog::_selectorEditKeyPress");
+ switch (event->keyval) {
+ case GDK_KEY_Escape:
+ selector->show();
+ selector_edit->hide();
+ selector_edit->get_style_context()->remove_class("system_error_color");
+ break;
+ }
+ return false;
+}
+
+bool StyleDialog::_on_foreach_iter(const Gtk::TreeModel::iterator &iter)
+{
+ g_debug("StyleDialog::_on_foreach_iter");
+
+ Gtk::TreeModel::Row row = *(iter);
+ Glib::ustring owner = row[_mColumns._colOwner];
+ if (owner.empty()) {
+ Glib::ustring value = _owner_style[row[_mColumns._colName]];
+ Glib::ustring tooltiptext = Glib::ustring(_("Invalid property set"));
+ if (!value.empty()) {
+ tooltiptext = Glib::ustring(_("Used in ") + _owner_style[row[_mColumns._colName]]);
+ }
+ row[_mColumns._colOwner] = tooltiptext;
+ }
+ return false;
+}
+
+void StyleDialog::_onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_onLinkObj");
+
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row && row[_mColumns._colLinked]) {
+ SPObject *linked = row[_mColumns._colHref];
+ if (linked) {
+ Inkscape::Selection *selection = getDesktop()->getSelection();
+ getDesktop()->getDocument()->setXMLDialogSelectedObject(linked);
+ selection->clear();
+ selection->set(linked);
+ }
+ }
+}
+
+/**
+ * @brief StyleDialog::_onPropDelete
+ * @param event
+ * @return true
+ * Delete the attribute from the style
+ */
+void StyleDialog::_onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_onPropDelete");
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Glib::ustring selector = row[_mColumns._colSelector];
+ row[_mColumns._colName] = "";
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ _deletion = true;
+ _writeStyleElement(store, selector);
+ }
+}
+
+void StyleDialog::_addOwnerStyle(Glib::ustring name, Glib::ustring selector)
+{
+ g_debug("StyleDialog::_addOwnerStyle");
+
+ if (_owner_style.find(name) == _owner_style.end()) {
+ _owner_style[name] = selector;
+ }
+}
+
+
+/**
+ * @brief StyleDialog::parseStyle
+ *
+ * Convert a style string into a vector map. This should be moved to style.cpp
+ *
+ */
+std::map<Glib::ustring, Glib::ustring> StyleDialog::parseStyle(Glib::ustring style_string)
+{
+ g_debug("StyleDialog::parseStyle");
+
+ std::map<Glib::ustring, Glib::ustring> ret;
+
+ REMOVE_SPACES(style_string); // We'd use const, but we need to trip spaces
+ std::vector<Glib::ustring> props = r_props->split(style_string);
+
+ for (auto token : props) {
+ REMOVE_SPACES(token);
+
+ if (token.empty())
+ break;
+ std::vector<Glib::ustring> pair = r_pair->split(token);
+
+ if (pair.size() > 1) {
+ ret[pair[0]] = pair[1];
+ }
+ }
+ return ret;
+}
+
+
+/**
+ * Update the content of the style element as selectors (or objects) are added/removed.
+ */
+void StyleDialog::_writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector,
+ Glib::ustring new_selector)
+{
+ g_debug("StyleDialog::_writeStyleElemen");
+ if (_updating) {
+ return;
+ }
+ _scroollock = true;
+ Inkscape::Selection *selection = getDesktop()->getSelection();
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = getDesktop()->getDocument()->getXMLDialogSelectedObject();
+ }
+ if (selection->objects().size() < 2 && !obj) {
+ readStyleElement();
+ return;
+ }
+ _updating = true;
+ gint selectorpos = 0;
+ std::string styleContent = "";
+ if (selector != "style_properties" && selector != "attributes") {
+ if (!new_selector.empty()) {
+ selector = new_selector;
+ }
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ for (auto selectoritem : selectordata) {
+ if (selectordata[selectordata.size() - 1] == selectoritem) {
+ selector = selectoritem;
+ } else {
+ styleContent = styleContent + selectoritem + ";\n";
+ }
+ }
+ styleContent = styleContent + "\n" + selector + " { \n";
+ }
+ selectorpos = _deleted_pos;
+ for (auto &row : store->children()) {
+ selector = row[_mColumns._colSelector];
+ selectorpos = row[_mColumns._colSelectorPos];
+ Glib::ustring opencomment = "";
+ Glib::ustring closecomment = "";
+ if (selector != "style_properties" && selector != "attributes") {
+ opencomment = row[_mColumns._colActive] ? " " : " /*";
+ closecomment = row[_mColumns._colActive] ? "\n" : "*/\n";
+ }
+ Glib::ustring name = row[_mColumns._colName];
+ Glib::ustring value = row[_mColumns._colValue];
+ if (!(name.empty() && value.empty())) {
+ styleContent = styleContent + opencomment + name + ":" + value + ";" + closecomment;
+ }
+ }
+ if (selector != "style_properties" && selector != "attributes") {
+ styleContent = styleContent + "}";
+ }
+ if (selector == "style_properties") {
+ _updating = true;
+ obj->getRepr()->setAttribute("style", styleContent, false);
+ _updating = false;
+ } else if (selector == "attributes") {
+ for (auto iter : obj->style->properties()) {
+ auto key = iter->id();
+ if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) {
+ const gchar *attr = obj->getRepr()->attribute(iter->name().c_str());
+ if (attr) {
+ _updating = true;
+ obj->getRepr()->removeAttribute(iter->name());
+ _updating = false;
+ }
+ }
+ }
+ for (auto &row : store->children()) {
+ Glib::ustring name = row[_mColumns._colName];
+ Glib::ustring value = row[_mColumns._colValue];
+ if (!(name.empty() && value.empty())) {
+ _updating = true;
+ obj->getRepr()->setAttribute(name, value);
+ _updating = false;
+ }
+ }
+ } else if (!selector.empty()) { // styleshet
+ // We could test if styleContent is empty and then delete the style node here but there is no
+ // harm in keeping it around ...
+
+ std::string pos = std::to_string(selectorpos);
+ std::string selectormatch = "(";
+ for (; selectorpos > 1; selectorpos--) {
+ selectormatch = selectormatch + "[^}]*?}";
+ }
+ selectormatch = selectormatch + ")([^}]*?})((.|\n)*)";
+
+ Inkscape::XML::Node *textNode = _getStyleTextNode(true);
+ std::regex e(selectormatch.c_str());
+ std::string content = (textNode->content() ? textNode->content() : "");
+ std::string result;
+ std::regex_replace(std::back_inserter(result), content.begin(), content.end(), e, "$1" + styleContent + "$3");
+ bool empty = false;
+ if (result.empty()) {
+ empty = true;
+ result = "* > .inkscapehacktmp{}";
+ }
+ textNode->setContent(result.c_str());
+ INKSCAPE.readStyleSheets(true);
+ if (empty) {
+ textNode->setContent("");
+ }
+ }
+ _updating = false;
+ readStyleElement();
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ for (auto iter : document->getObjectsBySelector(selector)) {
+ iter->style->readFromObject(iter);
+ iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_STYLE, _("Edited style element."));
+
+ g_debug("StyleDialog::_writeStyleElement(): | %s |", styleContent.c_str());
+}
+
+bool StyleDialog::_addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree,
+ Glib::ustring selector, gint pos)
+{
+ g_debug("StyleDialog::_addRow");
+
+ if (evt->type == GDK_BUTTON_RELEASE && evt->button == 1) {
+ Gtk::TreeIter iter = store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ Gtk::TreeModel::Row row = *(iter);
+ row[_mColumns._colSelector] = selector;
+ row[_mColumns._colSelectorPos] = pos;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = "";
+ row[_mColumns._colValue] = "";
+ row[_mColumns._colStrike] = false;
+ gint col = 2;
+ if (pos < 1) {
+ col = 1;
+ }
+ css_tree->show();
+ css_tree->set_cursor(path, *(css_tree->get_column(col)), true);
+ grab_focus();
+ return true;
+ }
+ return false;
+}
+
+void StyleDialog::_setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[])
+{
+ g_debug("StyleDialog::_setAutocompletion");
+
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column (_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(0);
+ entry_completion->set_popup_completion(true);
+ gint counter = 0;
+ const char * key = cssenum[counter].key;
+ while (key) {
+ Gtk::TreeModel::Row row = *(completionModel->prepend());
+ row[_mCSSData._colCSSData] = Glib::ustring(key);
+ counter++;
+ key = cssenum[counter].key;
+ }
+ entry->set_completion(entry_completion);
+}
+/*Harcode values non in enum*/
+void StyleDialog::_setAutocompletion(Gtk::Entry *entry, Glib::ustring name)
+{
+ g_debug("StyleDialog::_setAutocompletion");
+
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column(_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(0);
+ entry_completion->set_popup_completion(true);
+ if (name == "paint-order") {
+ Gtk::TreeModel::Row row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("fill markers stroke");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("fill stroke markers");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("stroke markers fill");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("stroke fill markers");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("markers fill stroke");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("markers stroke fill");
+ }
+ entry->set_completion(entry_completion);
+}
+
+void
+StyleDialog::_startValueEdit(Gtk::CellEditable* cell, const Glib::ustring& path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_startValueEdit");
+ _deletion = false;
+ _scroollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ Glib::ustring name = row[_mColumns._colName];
+ if (name == "paint-order") {
+ _setAutocompletion(entry, name);
+ } else if (name == "fill-rule") {
+ _setAutocompletion(entry, enum_fill_rule);
+ } else if (name == "stroke-linecap") {
+ _setAutocompletion(entry, enum_stroke_linecap);
+ } else if (name == "stroke-linejoin") {
+ _setAutocompletion(entry, enum_stroke_linejoin);
+ } else if (name == "font-style") {
+ _setAutocompletion(entry, enum_font_style);
+ } else if (name == "font-variant") {
+ _setAutocompletion(entry, enum_font_variant);
+ } else if (name == "font-weight") {
+ _setAutocompletion(entry, enum_font_weight);
+ } else if (name == "font-stretch") {
+ _setAutocompletion(entry, enum_font_stretch);
+ } else if (name == "font-variant-position") {
+ _setAutocompletion(entry, enum_font_variant_position);
+ } else if (name == "text-align") {
+ _setAutocompletion(entry, enum_text_align);
+ } else if (name == "text-transform") {
+ _setAutocompletion(entry, enum_text_transform);
+ } else if (name == "text-anchor") {
+ _setAutocompletion(entry, enum_text_anchor);
+ } else if (name == "white-space") {
+ _setAutocompletion(entry, enum_white_space);
+ } else if (name == "direction") {
+ _setAutocompletion(entry, enum_direction);
+ } else if (name == "baseline-shift") {
+ _setAutocompletion(entry, enum_baseline_shift);
+ } else if (name == "visibility") {
+ _setAutocompletion(entry, enum_visibility);
+ } else if (name == "overflow") {
+ _setAutocompletion(entry, enum_overflow);
+ } else if (name == "display") {
+ _setAutocompletion(entry, enum_display);
+ } else if (name == "shape-rendering") {
+ _setAutocompletion(entry, enum_shape_rendering);
+ } else if (name == "color-rendering") {
+ _setAutocompletion(entry, enum_color_rendering);
+ } else if (name == "overflow") {
+ _setAutocompletion(entry, enum_overflow);
+ } else if (name == "clip-rule") {
+ _setAutocompletion(entry, enum_clip_rule);
+ } else if (name == "color-interpolation") {
+ _setAutocompletion(entry, enum_color_interpolation);
+ }
+ entry->signal_key_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyReleased), entry));
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyPressed), entry));
+ }
+}
+
+void StyleDialog::_startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ _deletion = false;
+ g_debug("StyleDialog::_startNameEdit");
+ _scroollock = true;
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column(_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(1);
+ entry_completion->set_popup_completion(true);
+ for (auto prop : sp_attribute_name_list(true)) {
+ Gtk::TreeModel::Row row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = prop;
+ }
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->set_completion(entry_completion);
+ entry->signal_key_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyReleased), entry));
+ entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyPressed), entry));
+}
+
+
+gboolean sp_styledialog_store_move_to_next(gpointer data)
+{
+ StyleDialog *styledialog = reinterpret_cast<StyleDialog *>(data);
+ if (!styledialog->_deletion) {
+ auto selection = styledialog->_current_css_tree->get_selection();
+ Gtk::TreeIter iter = *(selection->get_selected());
+ Gtk::TreeModel::Path model = (Gtk::TreeModel::Path)iter;
+ if (model == styledialog->_current_path) {
+ styledialog->_current_css_tree->set_cursor(styledialog->_current_path, *styledialog->_current_value_col,
+ true);
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * @brief StyleDialog::nameEdited
+ * @param event
+ * @return
+ * Called when the name is edited in the TreeView editable column
+ */
+void StyleDialog::_nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store,
+ Gtk::TreeView *css_tree)
+{
+ g_debug("StyleDialog::_nameEdited");
+
+ _scroollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ _current_path = (Gtk::TreeModel::Path)*store->get_iter(path);
+
+ if (row) {
+ _current_css_tree = css_tree;
+ Glib::ustring finalname = name;
+ auto i = std::min(finalname.find(";"), finalname.find(":"));
+ if (i != std::string::npos) {
+ finalname.erase(i, name.size() - i);
+ }
+ gint pos = row[_mColumns._colSelectorPos];
+ bool write = false;
+ if (row[_mColumns._colName] != finalname && row[_mColumns._colValue] != "") {
+ write = true;
+ }
+ Glib::ustring selector = row[_mColumns._colSelector];
+ Glib::ustring value = row[_mColumns._colValue];
+ bool is_attr = selector == "attributes";
+ Glib::ustring old_name = row[_mColumns._colName];
+ row[_mColumns._colName] = finalname;
+ if (finalname.empty() && value.empty()) {
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ }
+ gint col = 3;
+ if (pos < 1 || is_attr) {
+ col = 2;
+ }
+ _current_value_col = css_tree->get_column(col);
+ if (write && old_name != name) {
+ _writeStyleElement(store, selector);
+ /*
+ I think is better comment this, is enoght update on value change
+ if (selector != "style_properties" && selector != "attributes") {
+ std::vector<SPObject *> objs = _getObjVec(selector);
+ for (auto obj : objs){
+ Glib::ustring css_str = "";
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
+ css->setAttribute(name, nullptr);
+ sp_repr_css_write_string(css, css_str);
+ obj->getRepr()->setAttribute("style", css_str);
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ } */
+ } else {
+ g_timeout_add(50, &sp_styledialog_store_move_to_next, this);
+ grab_focus();
+ }
+ }
+}
+
+/**
+ * @brief StyleDialog::valueEdited
+ * @param event
+ * @return
+ * Called when the value is edited in the TreeView editable column
+ */
+void StyleDialog::_valueEdited(const Glib::ustring &path, const Glib::ustring &value,
+ Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_valueEdited");
+
+ _scroollock = true;
+
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Glib::ustring finalvalue = value;
+ auto i = std::min(finalvalue.find(";"), finalvalue.find(":"));
+ if (i != std::string::npos) {
+ finalvalue.erase(i, finalvalue.size() - i);
+ }
+ Glib::ustring old_value = row[_mColumns._colValue];
+ if (old_value == finalvalue) {
+ return;
+ }
+ row[_mColumns._colValue] = finalvalue;
+ Glib::ustring selector = row[_mColumns._colSelector];
+ Glib::ustring name = row[_mColumns._colName];
+ if (name.empty() && finalvalue.empty()) {
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ }
+ _writeStyleElement(store, selector);
+ if (selector != "style_properties" && selector != "attributes") {
+ std::vector<SPObject *> objs = _getObjVec(selector);
+ for (auto obj : objs) {
+ Glib::ustring css_str = "";
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
+ css->removeAttribute(name);
+ sp_repr_css_write_string(css, css_str);
+ obj->getRepr()->setAttribute("style", css_str);
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+ }
+}
+
+void StyleDialog::_activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_activeToggled");
+
+ _scroollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ row[_mColumns._colActive] = !row[_mColumns._colActive];
+ Glib::ustring selector = row[_mColumns._colSelector];
+ _writeStyleElement(store, selector);
+ }
+}
+
+bool StyleDialog::_onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+bool StyleDialog::_onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_equal:
+ case GDK_KEY_colon:
+ entry->editing_done();
+ ret = true;
+ break;
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_semicolon: {
+ Glib::ustring text = entry->get_text();
+ auto i = std::min(text.find(";"), text.find(":"));
+ if (i != std::string::npos) {
+ entry->editing_done();
+ ret = true;
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+bool StyleDialog::_onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+bool StyleDialog::_onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_semicolon:
+ entry->editing_done();
+ ret = true;
+ break;
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_colon: {
+ Glib::ustring text = entry->get_text();
+ auto i = std::min(text.find(";"), text.find(":"));
+ if (i != std::string::npos) {
+ entry->editing_done();
+ ret = true;
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+/**
+ * Update the watchers on objects.
+ */
+void StyleDialog::_updateWatchers(SPDesktop *desktop)
+
+{
+ g_debug("StyleDialog::_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);
+ }
+}
+
+
+/**
+ * @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<SPObject *> StyleDialog::_getObjVec(Glib::ustring selector)
+{
+ g_debug("StyleDialog::_getObjVec");
+
+ g_assert(selector.find(";") == Glib::ustring::npos);
+
+ return getDesktop()->getDocument()->getObjectsBySelector(selector);
+}
+
+void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); }
+
+
+/**
+ * Handle document replaced. (Happens when a default document is immediately replaced by another
+ * document in a new window.)
+ */
+void StyleDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */)
+{
+ g_debug("StyleDialog::handleDocumentReplaced()");
+
+ _selection_changed_connection.disconnect();
+
+ _updateWatchers(desktop);
+
+ if (!desktop)
+ return;
+
+ _selection_changed_connection =
+ desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged)));
+
+ readStyleElement();
+}
+
+
+/*
+ * When a dialog is floating, it is connected to the active desktop.
+ */
+void StyleDialog::_handleDesktopChanged(SPDesktop *desktop)
+{
+ g_debug("StyleDialog::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, &StyleDialog::_handleSelectionChanged)));
+ _document_replaced_connection =
+ desktop->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced));
+
+ _updateWatchers(desktop);
+ readStyleElement();
+}
+
+
+/*
+ * Handle a change in which objects are selected in a document.
+ */
+void StyleDialog::_handleSelectionChanged()
+{
+ g_debug("StyleDialog::_handleSelectionChanged()");
+ _scroolpos = 0;
+ _vadj->set_value(0);
+ readStyleElement();
+}
+
+} // 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/styledialog.h b/src/ui/dialog/styledialog.h
new file mode 100644
index 0000000..2d96d48
--- /dev/null
+++ b/src/ui/dialog/styledialog.h
@@ -0,0 +1,208 @@
+// 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 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef STYLEDIALOG_H
+#define STYLEDIALOG_H
+
+#include "style-enums.h"
+#include <glibmm/regex.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/celleditable.h>
+#include <gtkmm/cellrenderercombo.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/tooltip.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treeselection.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/viewport.h>
+#include <ui/widget/panel.h>
+
+#include "ui/dialog/desktop-tracker.h"
+
+#include "xml/helper-observer.h"
+
+#include <memory>
+#include <vector>
+
+namespace Inkscape {
+
+XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing);
+
+namespace UI {
+namespace Dialog {
+
+/**
+ * @brief The StyleDialog 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 StyleDialog : public Widget::Panel {
+
+ public:
+ ~StyleDialog() override;
+ // No default constructor, noncopyable, nonassignable
+ StyleDialog();
+ StyleDialog(StyleDialog const &d) = delete;
+ StyleDialog operator=(StyleDialog const &d) = delete;
+
+ static StyleDialog &getInstance() { return *new StyleDialog(); }
+ void setCurrentSelector(Glib::ustring current_selector);
+ Gtk::TreeView *_current_css_tree;
+ Gtk::TreeViewColumn *_current_value_col;
+ Gtk::TreeModel::Path _current_path;
+ bool _deletion;
+ Glib::ustring fixCSSSelectors(Glib::ustring selector);
+ void readStyleElement();
+
+ private:
+ // Monitor <style> element for changes.
+ class NodeObserver;
+ // Monitor all objects for addition/removal/attribute change
+ class NodeWatcher;
+ Glib::RefPtr<Glib::Regex> r_props = Glib::Regex::create("\\s*;\\s*");
+ Glib::RefPtr<Glib::Regex> r_pair = Glib::Regex::create("\\s*:\\s*");
+ void _nodeAdded(Inkscape::XML::Node &repr);
+ void _nodeRemoved(Inkscape::XML::Node &repr);
+ void _nodeChanged(Inkscape::XML::Node &repr);
+ /* void _stylesheetChanged( Inkscape::XML::Node &repr ); */
+ // Data structure
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns()
+ {
+ add(_colActive);
+ add(_colName);
+ add(_colValue);
+ add(_colStrike);
+ add(_colSelector);
+ add(_colSelectorPos);
+ add(_colOwner);
+ add(_colLinked);
+ add(_colHref);
+ }
+ Gtk::TreeModelColumn<bool> _colActive; // Active or inactive property
+ Gtk::TreeModelColumn<Glib::ustring> _colName; // Name of the property.
+ Gtk::TreeModelColumn<Glib::ustring> _colValue; // Value of the property.
+ Gtk::TreeModelColumn<bool> _colStrike; // Property not used, overloaded
+ Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Style or matching object id.
+ Gtk::TreeModelColumn<gint> _colSelectorPos; // Position of the selector to handle dup selectors
+ Gtk::TreeModelColumn<Glib::ustring> _colOwner; // Store the owner of the property for popup
+ Gtk::TreeModelColumn<bool> _colLinked; // Other object linked
+ Gtk::TreeModelColumn<SPObject *> _colHref; // Is going to another object
+ };
+ ModelColumns _mColumns;
+
+ class CSSData : public Gtk::TreeModel::ColumnRecord {
+ public:
+ CSSData() { add(_colCSSData); }
+ Gtk::TreeModelColumn<Glib::ustring> _colCSSData; // Name of the property.
+ };
+ CSSData _mCSSData;
+ guint _deleted_pos;
+ // Widgets
+ Gtk::ScrolledWindow _scrolledWindow;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Box _mainBox;
+ Gtk::Box _styleBox;
+ // Reading and writing the style element.
+ Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false);
+ Glib::RefPtr<Gtk::TreeModel> _selectTree(Glib::ustring selector);
+ void _writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector,
+ Glib::ustring new_selector = "");
+ // void _selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry *selector_edit);
+ bool _selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector,
+ Gtk::Entry *selector_edit);
+ bool _selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit);
+ void _activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store);
+ bool _addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree,
+ Glib::ustring selector, gint pos);
+ void _onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store,
+ Gtk::TreeView *css_tree);
+ bool _onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ void _onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _valueEdited(const Glib::ustring &path, const Glib::ustring &value, Glib::RefPtr<Gtk::TreeStore> store);
+ void _startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+
+ void _startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]);
+ void _setAutocompletion(Gtk::Entry *entry, Glib::ustring name);
+ bool _on_foreach_iter(const Gtk::TreeModel::iterator &iter);
+ void _reload();
+ void _vscrool();
+ bool _scroollock;
+ double _scroolpos;
+ Glib::ustring _current_selector;
+
+ // Update watchers
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher;
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher;
+ void _updateWatchers(SPDesktop *);
+
+ // Manipulate Tree
+ std::vector<SPObject *> _getObjVec(Glib::ustring selector);
+ std::map<Glib::ustring, Glib::ustring> parseStyle(Glib::ustring style_string);
+ std::map<Glib::ustring, Glib::ustring> _owner_style;
+ void _addOwnerStyle(Glib::ustring name, Glib::ustring selector);
+ // Variables
+ Inkscape::XML::Node *m_root = nullptr;
+ Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver.
+ bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+
+ // Signals and handlers - External
+ sigc::connection _document_replaced_connection;
+ sigc::connection _desktop_changed_connection;
+ sigc::connection _selection_changed_connection;
+
+ void _handleDocumentReplaced(SPDesktop *desktop, SPDocument *document);
+ void _handleDesktopChanged(SPDesktop *desktop);
+ void _handleSelectionChanged();
+ void _closeDialog(Gtk::Dialog *textDialogPtr);
+ DesktopTracker _desktopTracker;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // STYLEDIALOG_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/svg-fonts-dialog.cpp b/src/ui/dialog/svg-fonts-dialog.cpp
new file mode 100644
index 0000000..9f191d1
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.cpp
@@ -0,0 +1,1067 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * SVG Fonts dialog - implementation.
+ */
+/* Authors:
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2008 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <message-stack.h>
+#include <sstream>
+
+#include <gtkmm/scale.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/imagemenuitem.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "selection.h"
+#include "svg-fonts-dialog.h"
+#include "verbs.h"
+
+#include "display/nr-svgfonts.h"
+#include "include/gtkmm_version.h"
+#include "object/sp-defs.h"
+#include "object/sp-font-face.h"
+#include "object/sp-font.h"
+#include "object/sp-glyph-kerning.h"
+#include "object/sp-glyph.h"
+#include "object/sp-missing-glyph.h"
+#include "svg/svg.h"
+#include "xml/repr.h"
+
+SvgFontDrawingArea::SvgFontDrawingArea():
+ _x(0),
+ _y(0),
+ _svgfont(nullptr),
+ _text()
+{
+}
+
+void SvgFontDrawingArea::set_svgfont(SvgFont* svgfont){
+ _svgfont = svgfont;
+}
+
+void SvgFontDrawingArea::set_text(Glib::ustring text){
+ _text = text;
+ redraw();
+}
+
+void SvgFontDrawingArea::set_size(int x, int y){
+ _x = x;
+ _y = y;
+ ((Gtk::Widget*) this)->set_size_request(_x, _y);
+}
+
+void SvgFontDrawingArea::redraw(){
+ ((Gtk::Widget*) this)->queue_draw();
+}
+
+bool SvgFontDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
+ if (_svgfont){
+ cr->set_font_face( Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_svgfont->get_font_face(), false /* does not have reference */)) );
+ cr->set_font_size (_y-20);
+ cr->move_to (10, 10);
+ cr->show_text (_text.c_str());
+
+ // Draw some lines to show line area.
+ cr->set_source_rgb( 0.5, 0.5, 0.5 );
+ cr->move_to ( 0, 10);
+ cr->line_to (_x, 10);
+ cr->stroke();
+ cr->move_to ( 0, _y-10);
+ cr->line_to (_x, _y-10);
+ cr->stroke();
+ }
+ return true;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+Gtk::HBox* SvgFontsDialog::AttrEntry(gchar* lbl, const SPAttributeEnum attr){
+ Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox());
+ hbox->add(* Gtk::manage(new Gtk::Label(lbl)) );
+ Gtk::Entry* entry = Gtk::manage(new Gtk::Entry());
+ hbox->add(* entry );
+ hbox->show_all();
+
+ entry->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_attr_changed));
+ return hbox;
+}
+*/
+
+SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr){
+ this->dialog = d;
+ this->attr = attr;
+ entry.set_tooltip_text(tooltip);
+ auto label = new Gtk::Label(lbl);
+ this->pack_start(*Gtk::manage(label), false, false, 4);
+ this->pack_end(entry, true, true);
+ this->show_all();
+
+ entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed));
+}
+
+void SvgFontsDialog::AttrEntry::set_text(char* t){
+ if (!t) return;
+ entry.set_text(t);
+}
+
+// 'font-family' has a problem as it is also a presentation attribute for <text>
+void SvgFontsDialog::AttrEntry::on_attr_changed(){
+
+ SPObject* o = nullptr;
+ for (auto& node: dialog->get_selected_spfont()->children) {
+ switch(this->attr){
+ case SP_PROP_FONT_FAMILY:
+ if (SP_IS_FONTFACE(&node)){
+ o = &node;
+ continue;
+ }
+ break;
+ default:
+ o = nullptr;
+ }
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ o->setAttribute((const gchar*) name, this->entry.get_text());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS,
+ _("Set SVG Font attribute"));
+ }
+
+}
+
+SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr) {
+
+ this->dialog = d;
+ this->attr = attr;
+ spin.set_tooltip_text(tooltip);
+ auto label = new Gtk::Label(lbl);
+ this->set_border_width(2);
+ this->set_spacing(6);
+ this->pack_start(*Gtk::manage(label), false, false);
+ this->pack_end(spin, true, true);
+ this->show_all();
+ spin.set_range(0, 4096);
+ spin.set_increments(16, 0);
+ spin.signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrSpin::on_attr_changed));
+}
+
+void SvgFontsDialog::AttrSpin::set_range(double low, double high){
+ spin.set_range(low, high);
+}
+
+void SvgFontsDialog::AttrSpin::set_value(double v){
+ spin.set_value(v);
+}
+
+void SvgFontsDialog::AttrSpin::on_attr_changed(){
+
+ SPObject* o = nullptr;
+ switch (this->attr) {
+
+ // <font> attributes
+ case SP_ATTR_HORIZ_ORIGIN_X:
+ case SP_ATTR_HORIZ_ORIGIN_Y:
+ case SP_ATTR_HORIZ_ADV_X:
+ case SP_ATTR_VERT_ORIGIN_X:
+ case SP_ATTR_VERT_ORIGIN_Y:
+ case SP_ATTR_VERT_ADV_Y:
+ o = this->dialog->get_selected_spfont();
+ break;
+
+ // <font-face> attributes
+ case SP_ATTR_UNITS_PER_EM:
+ case SP_ATTR_ASCENT:
+ case SP_ATTR_DESCENT:
+ case SP_ATTR_CAP_HEIGHT:
+ case SP_ATTR_X_HEIGHT:
+ for (auto& node: dialog->get_selected_spfont()->children){
+ if (SP_IS_FONTFACE(&node)){
+ o = &node;
+ continue;
+ }
+ }
+ break;
+
+ default:
+ o = nullptr;
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ std::ostringstream temp;
+ temp << this->spin.get_value();
+ o->setAttribute(name, temp.str());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS,
+ _("Set SVG Font attribute"));
+ }
+
+}
+
+Gtk::HBox* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttributeEnum /*attr*/){
+ Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox());
+ hbox->add(* Gtk::manage(new Gtk::Label(lbl)) );
+ hbox->add(* Gtk::manage(new Gtk::ComboBox()) );
+ hbox->show_all();
+ return hbox;
+}
+
+/*
+Gtk::HBox* SvgFontsDialog::AttrSpin(gchar* lbl){
+ Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox());
+ hbox->add(* Gtk::manage(new Gtk::Label(lbl)) );
+ hbox->add(* Gtk::manage(new Inkscape::UI::Widget::SpinBox()) );
+ hbox->show_all();
+ return hbox;
+}*/
+
+/*** SvgFontsDialog ***/
+
+GlyphComboBox::GlyphComboBox()= default;
+
+void GlyphComboBox::update(SPFont* spfont){
+ if (!spfont) return;
+
+ this->remove_all();
+
+ for (auto& node: spfont->children) {
+ if (SP_IS_GLYPH(&node)){
+ this->append((static_cast<SPGlyph*>(&node))->unicode);
+ }
+ }
+}
+
+void SvgFontsDialog::on_kerning_value_changed(){
+ if (!get_selected_kerning_pair()) {
+ return;
+ }
+
+ SPDocument* document = this->getDesktop()->getDocument();
+
+ //TODO: I am unsure whether this is the correct way of calling SPDocumentUndo::maybe_done
+ Glib::ustring undokey = "svgfonts:hkern:k:";
+ undokey += this->kerning_pair->u1->attribute_string();
+ undokey += ":";
+ undokey += this->kerning_pair->u2->attribute_string();
+
+ //slider values increase from right to left so that they match the kerning pair preview
+
+ //XML Tree being directly used here while it shouldn't be.
+ this->kerning_pair->setAttribute("k", Glib::Ascii::dtostr(get_selected_spfont()->horiz_adv_x - kerning_slider->get_value()));
+ DocumentUndo::maybeDone(document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS, _("Adjust kerning value"));
+
+ //populate_kerning_pairs_box();
+ kerning_preview.redraw();
+ _font_da.redraw();
+}
+
+void SvgFontsDialog::glyphs_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _GlyphsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::kerning_pairs_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _KerningPairsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::fonts_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _FontsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem)
+{
+ auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _GlyphsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _GlyphsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem)
+{
+ auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _KerningPairsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _KerningPairsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem)
+{
+ auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _FontsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _FontsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::update_sensitiveness(){
+ if (get_selected_spfont()){
+ global_vbox.set_sensitive(true);
+ glyphs_vbox.set_sensitive(true);
+ kerning_vbox.set_sensitive(true);
+ } else {
+ global_vbox.set_sensitive(false);
+ glyphs_vbox.set_sensitive(false);
+ kerning_vbox.set_sensitive(false);
+ }
+}
+
+/* Add all fonts in the document to the combobox. */
+void SvgFontsDialog::update_fonts()
+{
+ SPDesktop* desktop = this->getDesktop();
+ SPDocument* document = desktop->getDocument();
+ std::vector<SPObject *> fonts = document->getResourceList( "font" );
+
+ _model->clear();
+ for (auto font : fonts) {
+ Gtk::TreeModel::Row row = *_model->append();
+ SPFont* f = SP_FONT(font);
+ row[_columns.spfont] = f;
+ row[_columns.svgfont] = new SvgFont(f);
+ const gchar* lbl = f->label();
+ const gchar* id = f->getId();
+ row[_columns.label] = lbl ? lbl : (id ? id : "font");
+ }
+
+ update_sensitiveness();
+}
+
+void SvgFontsDialog::on_preview_text_changed(){
+ _font_da.set_text(_preview_entry.get_text());
+}
+
+void SvgFontsDialog::on_kerning_pair_selection_changed(){
+ SPGlyphKerning* kern = get_selected_kerning_pair();
+ if (!kern) {
+ kerning_preview.set_text("");
+ return;
+ }
+ Glib::ustring str;
+ str += kern->u1->sample_glyph();
+ str += kern->u2->sample_glyph();
+
+ kerning_preview.set_text(str);
+ this->kerning_pair = kern;
+
+ //slider values increase from right to left so that they match the kerning pair preview
+ kerning_slider->set_value(get_selected_spfont()->horiz_adv_x - kern->k);
+}
+
+void SvgFontsDialog::update_global_settings_tab(){
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+
+ _horiz_adv_x_spin->set_value(font->horiz_adv_x);
+ _horiz_origin_x_spin->set_value(font->horiz_origin_x);
+ _horiz_origin_y_spin->set_value(font->horiz_origin_y);
+
+ for (auto& obj: font->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ _familyname_entry->set_text((SP_FONTFACE(&obj))->font_family);
+ _units_per_em_spin->set_value((SP_FONTFACE(&obj))->units_per_em);
+ _ascent_spin->set_value((SP_FONTFACE(&obj))->ascent);
+ _descent_spin->set_value((SP_FONTFACE(&obj))->descent);
+ _x_height_spin->set_value((SP_FONTFACE(&obj))->x_height);
+ _cap_height_spin->set_value((SP_FONTFACE(&obj))->cap_height);
+ }
+ }
+}
+
+void SvgFontsDialog::on_font_selection_changed(){
+ SPFont* spfont = this->get_selected_spfont();
+ if (!spfont) return;
+
+ SvgFont* svgfont = this->get_selected_svgfont();
+ first_glyph.update(spfont);
+ second_glyph.update(spfont);
+ kerning_preview.set_svgfont(svgfont);
+ _font_da.set_svgfont(svgfont);
+ _font_da.redraw();
+
+ kerning_slider->set_range(0, spfont->horiz_adv_x);
+ kerning_slider->set_draw_value(false);
+ kerning_slider->set_value(0);
+
+ update_global_settings_tab();
+ populate_glyphs_box();
+ populate_kerning_pairs_box();
+ update_sensitiveness();
+}
+
+SPGlyphKerning* SvgFontsDialog::get_selected_kerning_pair()
+{
+ Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_KerningPairsListColumns.spnode];
+ return nullptr;
+}
+
+SvgFont* SvgFontsDialog::get_selected_svgfont()
+{
+ Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.svgfont];
+ return nullptr;
+}
+
+SPFont* SvgFontsDialog::get_selected_spfont()
+{
+ Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.spfont];
+ return nullptr;
+}
+
+SPGlyph* SvgFontsDialog::get_selected_glyph()
+{
+ Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_GlyphsListColumns.glyph_node];
+ return nullptr;
+}
+
+Gtk::VBox* SvgFontsDialog::global_settings_tab(){
+ _font_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ _horiz_adv_x_spin = new AttrSpin( this, (gchar*) _("Horiz. Advance X"), _("Average amount of horizontal space each letter takes up."), SP_ATTR_HORIZ_ADV_X);
+ _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin X"), _("Average horizontal origin location for each letter."), SP_ATTR_HORIZ_ORIGIN_X);
+ _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin Y"), _("Average vertical origin location for each letter."), SP_ATTR_HORIZ_ORIGIN_Y);
+ _font_face_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Face Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ _familyname_entry = new AttrEntry(this, (gchar*) _("Family Name:"), _("Name of the font as it appears in font selectors and css font-family properties."), SP_PROP_FONT_FAMILY);
+ _units_per_em_spin = new AttrSpin( this, (gchar*) _("Units per em"), _("Number of display units each letter takes up."), SP_ATTR_UNITS_PER_EM);
+ _ascent_spin = new AttrSpin( this, (gchar*) _("Ascent:"), _("Amount of space taken up by accenders like the tall line on the letter 'h'."), SP_ATTR_ASCENT);
+ _descent_spin = new AttrSpin( this, (gchar*) _("Descent:"), _("Amount of space taken up by decenders like the tail on the letter 'g'."), SP_ATTR_DESCENT);
+ _cap_height_spin = new AttrSpin( this, (gchar*) _("Cap Height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'."), SP_ATTR_CAP_HEIGHT);
+ _x_height_spin = new AttrSpin( this, (gchar*) _("x Height:"), _("The height of a lower-case letter above the baseline like the letter 'x'."), SP_ATTR_X_HEIGHT);
+
+ //_descent_spin->set_range(-4096,0);
+ _font_label->set_use_markup();
+ _font_face_label->set_use_markup();
+
+ global_vbox.set_border_width(2);
+ global_vbox.pack_start(*_font_label);
+ global_vbox.pack_start(*_horiz_adv_x_spin);
+ global_vbox.pack_start(*_horiz_origin_x_spin);
+ global_vbox.pack_start(*_horiz_origin_y_spin);
+ global_vbox.pack_start(*_font_face_label);
+ global_vbox.pack_start(*_familyname_entry);
+ global_vbox.pack_start(*_units_per_em_spin);
+ global_vbox.pack_start(*_ascent_spin);
+ global_vbox.pack_start(*_descent_spin);
+ global_vbox.pack_start(*_cap_height_spin);
+ global_vbox.pack_start(*_x_height_spin);
+
+/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SP_PROP_FONT_STYLE));
+ global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SP_PROP_FONT_VARIANT));
+ global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SP_PROP_FONT_WEIGHT));
+*/
+
+ return &global_vbox;
+}
+
+void
+SvgFontsDialog::populate_glyphs_box()
+{
+ if (!_GlyphsListStore) return;
+ _GlyphsListStore->clear();
+
+ SPFont* spfont = this->get_selected_spfont();
+ _glyphs_observer.set(spfont);
+
+ for (auto& node: spfont->children) {
+ if (SP_IS_GLYPH(&node)){
+ Gtk::TreeModel::Row row = *(_GlyphsListStore->append());
+ row[_GlyphsListColumns.glyph_node] = static_cast<SPGlyph*>(&node);
+ row[_GlyphsListColumns.glyph_name] = (static_cast<SPGlyph*>(&node))->glyph_name;
+ row[_GlyphsListColumns.unicode] = (static_cast<SPGlyph*>(&node))->unicode;
+ row[_GlyphsListColumns.advance] = (static_cast<SPGlyph*>(&node))->horiz_adv_x;
+ }
+ }
+}
+
+void
+SvgFontsDialog::populate_kerning_pairs_box()
+{
+ if (!_KerningPairsListStore) return;
+ _KerningPairsListStore->clear();
+
+ SPFont* spfont = this->get_selected_spfont();
+
+ for (auto& node: spfont->children) {
+ if (SP_IS_HKERN(&node)){
+ Gtk::TreeModel::Row row = *(_KerningPairsListStore->append());
+ row[_KerningPairsListColumns.first_glyph] = (static_cast<SPGlyphKerning*>(&node))->u1->attribute_string().c_str();
+ row[_KerningPairsListColumns.second_glyph] = (static_cast<SPGlyphKerning*>(&node))->u2->attribute_string().c_str();
+ row[_KerningPairsListColumns.kerning_value] = (static_cast<SPGlyphKerning*>(&node))->k;
+ row[_KerningPairsListColumns.spnode] = static_cast<SPGlyphKerning*>(&node);
+ }
+ }
+}
+
+SPGlyph *new_glyph(SPDocument* document, SPFont *font, const int count)
+{
+ g_return_val_if_fail(font != nullptr, NULL);
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+
+ // create a new glyph
+ Inkscape::XML::Node *repr;
+ repr = xml_doc->createElement("svg:glyph");
+
+ std::ostringstream os;
+ os << _("glyph") << " " << count;
+ repr->setAttribute("glyph-name", os.str());
+
+ // Append the new glyph node to the current font
+ font->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // get corresponding object
+ SPGlyph *g = SP_GLYPH( document->getObjectByRepr(repr) );
+
+ g_assert(g != nullptr);
+ g_assert(SP_IS_GLYPH(g));
+
+ return g;
+}
+
+void SvgFontsDialog::update_glyphs(){
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+ populate_glyphs_box();
+ populate_kerning_pairs_box();
+ first_glyph.update(font);
+ second_glyph.update(font);
+ get_selected_svgfont()->refresh();
+ _font_da.redraw();
+}
+
+void SvgFontsDialog::add_glyph(){
+ const int count = _GlyphsListStore->children().size();
+ SPDocument* doc = this->getDesktop()->getDocument();
+ /* SPGlyph* glyph =*/ new_glyph(doc, get_selected_spfont(), count+1);
+
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add glyph"));
+
+ update_glyphs();
+}
+
+Geom::PathVector
+SvgFontsDialog::flip_coordinate_system(Geom::PathVector pathv){
+ double units_per_em = 1024;
+ for (auto& obj: get_selected_spfont()->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_get_double(obj.getRepr(), "units-per-em", &units_per_em);
+ }
+ }
+ double baseline_offset = units_per_em - get_selected_spfont()->horiz_origin_y;
+ //This matrix flips y-axis and places the origin at baseline
+ Geom::Affine m(Geom::Coord(1),Geom::Coord(0),Geom::Coord(0),Geom::Coord(-1),Geom::Coord(0),Geom::Coord(baseline_offset));
+ return pathv*m;
+}
+
+void SvgFontsDialog::set_glyph_description_from_selected_path(){
+ SPDesktop* desktop = this->getDesktop();
+ if (!desktop) {
+ g_warning("SvgFontsDialog: No active desktop");
+ return;
+ }
+
+ Inkscape::MessageStack *msgStack = desktop->getMessageStack();
+ SPDocument* doc = desktop->getDocument();
+ Inkscape::Selection* sel = desktop->getSelection();
+ if (sel->isEmpty()){
+ char *msg = _("Select a <b>path</b> to define the curves of a glyph");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Inkscape::XML::Node* node = sel->xmlNodes().front();
+ if (!node) return;//TODO: should this be an assert?
+ if (!node->matchAttributeName("d") || !node->attribute("d")){
+ char *msg = _("The selected object does not have a <b>path</b> description.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ } //TODO: //Is there a better way to tell it to to the user?
+
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph){
+ char *msg = _("No glyph selected in the SVGFonts dialog.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d"));
+
+ //XML Tree being directly used here while it shouldn't be.
+ gchar *str = sp_svg_write_path (flip_coordinate_system(pathv));
+ glyph->setAttribute("d", str);
+ g_free(str);
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves"));
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::missing_glyph_description_from_selected_path(){
+ SPDesktop* desktop = this->getDesktop();
+ if (!desktop) {
+ g_warning("SvgFontsDialog: No active desktop");
+ return;
+ }
+
+ Inkscape::MessageStack *msgStack = desktop->getMessageStack();
+ SPDocument* doc = desktop->getDocument();
+ Inkscape::Selection* sel = desktop->getSelection();
+ if (sel->isEmpty()){
+ char *msg = _("Select a <b>path</b> to define the curves of a glyph");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Inkscape::XML::Node* node = sel->xmlNodes().front();
+ if (!node) return;//TODO: should this be an assert?
+ if (!node->matchAttributeName("d") || !node->attribute("d")){
+ char *msg = _("The selected object does not have a <b>path</b> description.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ } //TODO: //Is there a better way to tell it to to the user?
+
+ Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d"));
+
+ for (auto& obj: get_selected_spfont()->children) {
+ if (SP_IS_MISSING_GLYPH(&obj)){
+
+ //XML Tree being directly used here while it shouldn't be.
+ gchar *str = sp_svg_write_path (flip_coordinate_system(pathv));
+ obj.setAttribute("d", str);
+ g_free(str);
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves"));
+ }
+ }
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::reset_missing_glyph_description(){
+ SPDesktop* desktop = this->getDesktop();
+ if (!desktop) {
+ g_warning("SvgFontsDialog: No active desktop");
+ return;
+ }
+
+ SPDocument* doc = desktop->getDocument();
+ for (auto& obj: get_selected_spfont()->children) {
+ if (SP_IS_MISSING_GLYPH(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("d", "M0,0h1000v1024h-1000z");
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Reset missing-glyph"));
+ }
+ }
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){
+ Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected();
+ if (!i) return;
+
+ SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node];
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("glyph-name", str);
+
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Edit glyph name"));
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){
+ Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected();
+ if (!i) return;
+
+ SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node];
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("unicode", str);
+
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph unicode"));
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){
+ Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected();
+ if (!i) return;
+
+ SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node];
+ //XML Tree being directly used here while it shouldn't be.
+ std::istringstream is(str);
+ double value;
+ // Check if input valid
+ if ((is >> value)) {
+ glyph->setAttribute("horiz-adv-x", str);
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph advance"));
+
+ update_glyphs();
+ } else {
+ std::cerr << "SvgFontDialog::glyph_advance_edit: Error in input: " << str << std::endl;
+ }
+}
+
+void SvgFontsDialog::remove_selected_font(){
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(font->getRepr());
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove font"));
+
+ update_fonts();
+}
+
+void SvgFontsDialog::remove_selected_glyph(){
+ if(!_GlyphsList.get_selection()) return;
+
+ Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected();
+ if(!i) return;
+
+ SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node];
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(glyph->getRepr());
+
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove glyph"));
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::remove_selected_kerning_pair(){
+ if(!_KerningPairsList.get_selection()) return;
+
+ Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected();
+ if(!i) return;
+
+ SPGlyphKerning* pair = (*i)[_KerningPairsListColumns.spnode];
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(pair->getRepr());
+
+ SPDocument* doc = this->getDesktop()->getDocument();
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove kerning pair"));
+
+ update_glyphs();
+}
+
+Gtk::VBox* SvgFontsDialog::glyphs_tab(){
+ _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release));
+ create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph));
+
+ Gtk::HBox* missing_glyph_hbox = Gtk::manage(new Gtk::HBox(false, 4));
+ Gtk::Label* missing_glyph_label = Gtk::manage(new Gtk::Label(_("Missing Glyph:")));
+ missing_glyph_hbox->set_hexpand(false);
+ missing_glyph_hbox->pack_start(*missing_glyph_label, false,false);
+ missing_glyph_hbox->pack_start(missing_glyph_button, false,false);
+ missing_glyph_hbox->pack_start(missing_glyph_reset_button, false,false);
+
+ missing_glyph_button.set_label(_("From selection..."));
+ missing_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::missing_glyph_description_from_selected_path));
+ missing_glyph_reset_button.set_label(_("Reset"));
+ missing_glyph_reset_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description));
+
+ glyphs_vbox.set_border_width(4);
+ glyphs_vbox.set_spacing(4);
+ glyphs_vbox.pack_start(*missing_glyph_hbox, false,false);
+
+ glyphs_vbox.add(_GlyphsListScroller);
+ _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _GlyphsListScroller.set_size_request(-1, 290);
+ _GlyphsListScroller.add(_GlyphsList);
+ _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns);
+ _GlyphsList.set_model(_GlyphsListStore);
+ _GlyphsList.append_column_editable(_("Glyph name"), _GlyphsListColumns.glyph_name);
+ _GlyphsList.append_column_editable(_("Matching string"), _GlyphsListColumns.unicode);
+ _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f");
+ Gtk::HBox* hb = Gtk::manage(new Gtk::HBox(false, 4));
+ add_glyph_button.set_label(_("Add Glyph"));
+ add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph));
+
+ hb->pack_start(add_glyph_button, false,false);
+ hb->pack_start(glyph_from_path_button, false,false);
+
+ glyphs_vbox.pack_start(*hb, false, false);
+ glyph_from_path_button.set_label(_("Get curves from selection..."));
+ glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path));
+
+ dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(0))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit));
+
+ dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(1))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit));
+
+ dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(2))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit));
+
+ _glyphs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::update_glyphs));
+
+ return &glyphs_vbox;
+}
+
+void SvgFontsDialog::add_kerning_pair(){
+ if (first_glyph.get_active_text() == "" ||
+ second_glyph.get_active_text() == "") return;
+
+ //look for this kerning pair on the currently selected font
+ this->kerning_pair = nullptr;
+ for (auto& node: get_selected_spfont()->children) {
+ //TODO: It is not really correct to get only the first byte of each string.
+ //TODO: We should also support vertical kerning
+ if (SP_IS_HKERN(&node) && (static_cast<SPGlyphKerning*>(&node))->u1->contains((gchar) first_glyph.get_active_text().c_str()[0])
+ && (static_cast<SPGlyphKerning*>(&node))->u2->contains((gchar) second_glyph.get_active_text().c_str()[0]) ){
+ this->kerning_pair = static_cast<SPGlyphKerning*>(&node);
+ continue;
+ }
+ }
+
+ if (this->kerning_pair) return; //We already have this kerning pair
+
+ SPDocument* document = this->getDesktop()->getDocument();
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+
+ // create a new hkern node
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:hkern");
+
+ repr->setAttribute("u1", first_glyph.get_active_text());
+ repr->setAttribute("u2", second_glyph.get_active_text());
+ repr->setAttribute("k", "0");
+
+ // Append the new hkern node to the current font
+ get_selected_spfont()->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // get corresponding object
+ this->kerning_pair = SP_HKERN( document->getObjectByRepr(repr) );
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_SVG_FONTS, _("Add kerning pair"));
+}
+
+Gtk::VBox* SvgFontsDialog::kerning_tab(){
+ _KerningPairsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::kerning_pairs_list_button_release));
+ create_kerning_pairs_popup_menu(_KerningPairsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_kerning_pair));
+
+//Kerning Setup:
+ kerning_vbox.set_border_width(4);
+ kerning_vbox.set_spacing(4);
+ // kerning_vbox.add(*Gtk::manage(new Gtk::Label(_("Kerning Setup"))));
+ Gtk::HBox* kerning_selector = Gtk::manage(new Gtk::HBox());
+ kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("1st Glyph:"))), false, false);
+ kerning_selector->pack_start(first_glyph, true, true, 4);
+ kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("2nd Glyph:"))), false, false);
+ kerning_selector->pack_start(second_glyph, true, true, 4);
+ kerning_selector->pack_start(add_kernpair_button, true, true);
+ add_kernpair_button.set_label(_("Add pair"));
+ add_kernpair_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_kerning_pair));
+ _KerningPairsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_pair_selection_changed));
+ kerning_slider->signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_value_changed));
+
+ kerning_vbox.pack_start(*kerning_selector, false,false);
+
+ kerning_vbox.pack_start(_KerningPairsListScroller, true,true);
+ _KerningPairsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _KerningPairsListScroller.add(_KerningPairsList);
+ _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns);
+ _KerningPairsList.set_model(_KerningPairsListStore);
+ _KerningPairsList.append_column(_("First Unicode range"), _KerningPairsListColumns.first_glyph);
+ _KerningPairsList.append_column(_("Second Unicode range"), _KerningPairsListColumns.second_glyph);
+// _KerningPairsList.append_column_numeric_editable(_("Kerning Value"), _KerningPairsListColumns.kerning_value, "%f");
+
+ kerning_vbox.pack_start((Gtk::Widget&) kerning_preview, false,false);
+
+ // kerning_slider has a big handle. Extra padding added
+ Gtk::HBox* kerning_amount_hbox = Gtk::manage(new Gtk::HBox(false, 8));
+ kerning_vbox.pack_start(*kerning_amount_hbox, false,false);
+ kerning_amount_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Kerning Value:"))), false,false);
+ kerning_amount_hbox->pack_start(*kerning_slider, true,true);
+
+ kerning_preview.set_size(300 + 20, 150 + 20);
+ _font_da.set_size(300 + 50 + 20, 60 + 20);
+
+ return &kerning_vbox;
+}
+
+SPFont *new_font(SPDocument *document)
+{
+ g_return_val_if_fail(document != nullptr, NULL);
+
+ SPDefs *defs = document->getDefs();
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+
+ // create a new font
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:font");
+
+ //By default, set the horizontal advance to 1024 units
+ repr->setAttribute("horiz-adv-x", "1024");
+
+ // Append the new font node to defs
+ defs->getRepr()->appendChild(repr);
+
+ //create a missing glyph
+ Inkscape::XML::Node *fontface;
+ fontface = xml_doc->createElement("svg:font-face");
+ fontface->setAttribute("units-per-em", "1024");
+ repr->appendChild(fontface);
+
+ //create a missing glyph
+ Inkscape::XML::Node *mg;
+ mg = xml_doc->createElement("svg:missing-glyph");
+ mg->setAttribute("d", "M0,0h1000v1024h-1000z");
+ repr->appendChild(mg);
+
+ // get corresponding object
+ SPFont *f = SP_FONT( document->getObjectByRepr(repr) );
+
+ g_assert(f != nullptr);
+ g_assert(SP_IS_FONT(f));
+ Inkscape::GC::release(mg);
+ Inkscape::GC::release(repr);
+ return f;
+}
+
+void set_font_family(SPFont* font, char* str){
+ if (!font) return;
+ for (auto& obj: font->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", str);
+ }
+ }
+
+ DocumentUndo::done(font->document, SP_VERB_DIALOG_SVG_FONTS, _("Set font family"));
+}
+
+void SvgFontsDialog::add_font(){
+ SPDocument* doc = this->getDesktop()->getDocument();
+ SPFont* font = new_font(doc);
+
+ const int count = _model->children().size();
+ std::ostringstream os, os2;
+ os << _("font") << " " << count;
+ font->setLabel(os.str().c_str());
+
+ os2 << "SVGFont " << count;
+ for (auto& obj: font->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", os2.str());
+ }
+ }
+
+ update_fonts();
+// select_font(font);
+
+ DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add font"));
+}
+
+SvgFontsDialog::SvgFontsDialog()
+ : UI::Widget::Panel("/dialogs/svgfonts", SP_VERB_DIALOG_SVG_FONTS),
+ _add(_("_New"), true)
+{
+ kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+ _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font));
+
+ Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox());
+ Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox());
+
+ vbox->pack_start(_FontsList);
+ vbox->pack_start(_add, false, false);
+ hbox->add(*vbox);
+ hbox->add(_font_settings);
+ _getContents()->add(*hbox);
+
+//List of SVGFonts declared in a document:
+ _model = Gtk::ListStore::create(_columns);
+ _FontsList.set_model(_model);
+ _FontsList.append_column_editable(_("_Fonts"), _columns.label);
+ _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed));
+
+ this->update_fonts();
+
+ Gtk::Notebook *tabs = Gtk::manage(new Gtk::Notebook());
+ tabs->set_scrollable();
+
+ tabs->append_page(*global_settings_tab(), _("_Global Settings"), true);
+ tabs->append_page(*glyphs_tab(), _("_Glyphs"), true);
+ tabs->append_page(*kerning_tab(), _("_Kerning"), true);
+
+ _font_settings.add(*tabs);
+
+//Text Preview:
+ _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed));
+ _getContents()->pack_start((Gtk::Widget&) _font_da, false, false);
+ _preview_entry.set_text(_("Sample Text"));
+ _font_da.set_text(_("Sample Text"));
+
+ Gtk::HBox* preview_entry_hbox = Gtk::manage(new Gtk::HBox(false, 4));
+ _getContents()->pack_start(*preview_entry_hbox, false, false); // Non-latin characters may need more height.
+ preview_entry_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Preview Text:"))), false, false);
+ preview_entry_hbox->pack_start(_preview_entry, true, true);
+
+ _FontsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::fonts_list_button_release));
+ create_fonts_popup_menu(_FontsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_font));
+
+ _defs_observer.set(this->getDesktop()->getDocument()->getDefs());
+ _defs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::update_fonts));
+
+ _getContents()->show_all();
+}
+
+SvgFontsDialog::~SvgFontsDialog()= default;
+
+} // 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/svg-fonts-dialog.h b/src/ui/dialog/svg-fonts-dialog.h
new file mode 100644
index 0000000..ae07d7f
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.h
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief SVG Fonts dialog
+ */
+/* Authors:
+ * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org>
+ *
+ * Copyright (C) 2008 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_H
+#define INKSCAPE_UI_DIALOG_SVG_FONTS_H
+
+#include "ui/widget/panel.h"
+#include <2geom/pathvector.h>
+#include "ui/widget/spinbutton.h"
+
+#include <gtkmm/box.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+
+#include "attributes.h"
+#include "xml/helper-observer.h"
+
+namespace Gtk {
+class Scale;
+}
+
+class SPGlyph;
+class SPGlyphKerning;
+class SvgFont;
+
+class SvgFontDrawingArea : Gtk::DrawingArea{
+public:
+ SvgFontDrawingArea();
+ void set_text(Glib::ustring);
+ void set_svgfont(SvgFont*);
+ void set_size(int x, int y);
+ void redraw();
+private:
+ int _x,_y;
+ SvgFont* _svgfont;
+ Glib::ustring _text;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+};
+
+class SPFont;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class GlyphComboBox : public Gtk::ComboBoxText {
+public:
+ GlyphComboBox();
+ void update(SPFont*);
+};
+
+class SvgFontsDialog : public UI::Widget::Panel {
+public:
+ SvgFontsDialog();
+ ~SvgFontsDialog() override;
+
+ static SvgFontsDialog &getInstance() { return *new SvgFontsDialog(); }
+
+ void update_fonts();
+ SvgFont* get_selected_svgfont();
+ SPFont* get_selected_spfont();
+ SPGlyph* get_selected_glyph();
+ SPGlyphKerning* get_selected_kerning_pair();
+
+ //TODO: these methods should be private, right?!
+ void on_font_selection_changed();
+ void on_kerning_pair_selection_changed();
+ void on_preview_text_changed();
+ void on_kerning_pair_changed();
+ void on_kerning_value_changed();
+ void on_setfontdata_changed();
+ void add_font();
+ Geom::PathVector flip_coordinate_system(Geom::PathVector pathv);
+ bool updating;
+
+ // Used for font-family
+ class AttrEntry : public Gtk::HBox
+ {
+ public:
+ AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr);
+ void set_text(char*);
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Gtk::Entry entry;
+ SPAttributeEnum attr;
+ };
+
+ class AttrSpin : public Gtk::HBox
+ {
+ public:
+ AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr);
+ void set_value(double v);
+ void set_range(double low, double high);
+ Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; }
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Inkscape::UI::Widget::SpinButton spin;
+ SPAttributeEnum attr;
+ };
+
+private:
+ void update_glyphs();
+ void update_sensitiveness();
+ void update_global_settings_tab();
+ void populate_glyphs_box();
+ void populate_kerning_pairs_box();
+ void set_glyph_description_from_selected_path();
+ void missing_glyph_description_from_selected_path();
+ void reset_missing_glyph_description();
+ void add_glyph();
+ void glyph_unicode_edit(const Glib::ustring&, const Glib::ustring&);
+ void glyph_name_edit( const Glib::ustring&, const Glib::ustring&);
+ void glyph_advance_edit(const Glib::ustring&, const Glib::ustring&);
+ void remove_selected_glyph();
+ void remove_selected_font();
+ void remove_selected_kerning_pair();
+
+ void add_kerning_pair();
+
+ void create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem);
+ void glyphs_list_button_release(GdkEventButton* event);
+
+ void create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem);
+ void fonts_list_button_release(GdkEventButton* event);
+
+ void create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem);
+ void kerning_pairs_list_button_release(GdkEventButton* event);
+
+ Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts
+ Inkscape::XML::SignalObserver _glyphs_observer;
+
+ Gtk::HBox* AttrCombo(gchar* lbl, const SPAttributeEnum attr);
+// Gtk::HBox* AttrSpin(gchar* lbl, const SPAttributeEnum attr);
+ Gtk::VBox* global_settings_tab();
+
+ // <font>
+ Gtk::Label* _font_label;
+ AttrSpin* _horiz_adv_x_spin;
+ AttrSpin* _horiz_origin_x_spin;
+ AttrSpin* _horiz_origin_y_spin;
+
+ // <font-face>
+ Gtk::Label* _font_face_label;
+ AttrEntry* _familyname_entry;
+ AttrSpin* _units_per_em_spin;
+ AttrSpin* _ascent_spin;
+ AttrSpin* _descent_spin;
+ AttrSpin* _cap_height_spin;
+ AttrSpin* _x_height_spin;
+
+ Gtk::VBox* kerning_tab();
+ Gtk::VBox* glyphs_tab();
+ Gtk::Button _add;
+ Gtk::Button add_glyph_button;
+ Gtk::Button glyph_from_path_button;
+ Gtk::Button missing_glyph_button;
+ Gtk::Button missing_glyph_reset_button;
+
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(spfont);
+ add(svgfont);
+ add(label);
+ }
+
+ Gtk::TreeModelColumn<SPFont*> spfont;
+ Gtk::TreeModelColumn<SvgFont*> svgfont;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ };
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Columns _columns;
+ Gtk::TreeView _FontsList;
+
+ class GlyphsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ GlyphsColumns()
+ {
+ add(glyph_node);
+ add(glyph_name);
+ add(unicode);
+ add(advance);
+ }
+
+ Gtk::TreeModelColumn<SPGlyph*> glyph_node;
+ Gtk::TreeModelColumn<Glib::ustring> glyph_name;
+ Gtk::TreeModelColumn<Glib::ustring> unicode;
+ Gtk::TreeModelColumn<double> advance;
+ };
+ GlyphsColumns _GlyphsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _GlyphsListStore;
+ Gtk::TreeView _GlyphsList;
+ Gtk::ScrolledWindow _GlyphsListScroller;
+
+ class KerningPairColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ KerningPairColumns()
+ {
+ add(first_glyph);
+ add(second_glyph);
+ add(kerning_value);
+ add(spnode);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> first_glyph;
+ Gtk::TreeModelColumn<Glib::ustring> second_glyph;
+ Gtk::TreeModelColumn<double> kerning_value;
+ Gtk::TreeModelColumn<SPGlyphKerning*> spnode;
+ };
+ KerningPairColumns _KerningPairsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _KerningPairsListStore;
+ Gtk::TreeView _KerningPairsList;
+ Gtk::ScrolledWindow _KerningPairsListScroller;
+ Gtk::Button add_kernpair_button;
+
+ Gtk::VBox _font_settings;
+ Gtk::VBox global_vbox;
+ Gtk::VBox glyphs_vbox;
+ Gtk::VBox kerning_vbox;
+ Gtk::Entry _preview_entry;
+
+ Gtk::Menu _FontsContextMenu;
+ Gtk::Menu _GlyphsContextMenu;
+ Gtk::Menu _KerningPairsContextMenu;
+
+ SvgFontDrawingArea _font_da, kerning_preview;
+ GlyphComboBox first_glyph, second_glyph;
+ SPGlyphKerning* kerning_pair;
+ Inkscape::UI::Widget::SpinButton setwidth_spin;
+ Gtk::Scale* kerning_slider;
+
+ class EntryWidget : public Gtk::HBox
+ {
+ public:
+ EntryWidget()
+ {
+ this->add(this->_label);
+ this->add(this->_entry);
+ }
+ void set_label(const gchar* l){
+ this->_label.set_text(l);
+ }
+ private:
+ Gtk::Label _label;
+ Gtk::Entry _entry;
+ };
+ EntryWidget _font_family, _font_variant;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_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/svg-preview.cpp b/src/ui/dialog/svg-preview.cpp
new file mode 100644
index 0000000..0e83ab8
--- /dev/null
+++ b/src/ui/dialog/svg-preview.cpp
@@ -0,0 +1,476 @@
+// 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 <johan@shouraizou.nl>
+ * 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 <iostream>
+#include <fstream>
+
+#include <glibmm/i18n.h>
+#include <glib/gstdio.h> // GStatBuf
+
+#include "svg-preview.h"
+
+#include "document.h"
+#include "ui/view/svg-view-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*#########################################################################
+### SVG Preview Widget
+#########################################################################*/
+
+bool SVGPreview::setDocument(SPDocument *doc)
+{
+ if (viewer) {
+ viewer->setDocument(doc);
+ } else {
+ viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc));
+ pack_start(*viewer, true, true);
+ }
+
+ if (document) {
+ delete document;
+ }
+ document = doc;
+
+ show_all();
+
+ return true;
+}
+
+
+bool SVGPreview::setFileName(Glib::ustring &theFileName)
+{
+ Glib::ustring fileName = theFileName;
+
+ fileName = Glib::filename_to_utf8(fileName);
+
+ /**
+ * I don't know why passing false to keepalive is bad. But it
+ * prevents the display of an svg with a non-ascii filename
+ */
+ SPDocument *doc = SPDocument::createNewDoc(fileName.c_str(), true);
+ if (!doc) {
+ g_warning("SVGView: error loading document '%s'\n", fileName.c_str());
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+
+
+bool SVGPreview::setFromMem(char const *xmlBuffer)
+{
+ if (!xmlBuffer)
+ return false;
+
+ gint len = (gint)strlen(xmlBuffer);
+ SPDocument *doc = SPDocument::createNewDocFromMem(xmlBuffer, len, false);
+ if (!doc) {
+ g_warning("SVGView: error loading buffer '%s'\n", xmlBuffer);
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+
+
+void SVGPreview::showImage(Glib::ustring &theFileName)
+{
+ Glib::ustring fileName = theFileName;
+
+ // Let's get real width and height from SVG file. These are template
+ // files so we assume they are well formed.
+
+ // std::cout << "SVGPreview::showImage: " << theFileName << std::endl;
+ std::string width;
+ std::string height;
+
+ /*#####################################
+ # LET'S HAVE SOME FUN WITH SVG!
+ # Instead of just loading an image, why
+ # don't we make a lovely little svg and
+ # display it nicely?
+ #####################################*/
+
+ // Arbitrary size of svg doc -- rather 'portrait' shaped
+ gint previewWidth = 400;
+ gint previewHeight = 600;
+
+ // Get some image info. Smart pointer does not need to be deleted
+ Glib::RefPtr<Gdk::Pixbuf> img(nullptr);
+ try
+ {
+ img = Gdk::Pixbuf::create_from_file(fileName);
+ }
+ catch (const Glib::FileError &e)
+ {
+ g_message("caught Glib::FileError in SVGPreview::showImage");
+ return;
+ }
+ catch (const Gdk::PixbufError &e)
+ {
+ g_message("Gdk::PixbufError in SVGPreview::showImage");
+ return;
+ }
+ catch (...)
+ {
+ g_message("Caught ... in SVGPreview::showImage");
+ return;
+ }
+
+ gint imgWidth = img->get_width();
+ gint imgHeight = img->get_height();
+
+ Glib::ustring svg = ".svg";
+ if (hasSuffix(fileName, svg)) {
+ std::ifstream input(theFileName.c_str());
+ if( !input ) {
+ std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName << std::endl;
+ } else {
+
+ std::string token;
+
+ Glib::MatchInfo match_info;
+ Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("width=\"(.*)\"");
+ Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create("height=\"(.*)\"");
+
+ while( !input.eof() && (height.empty() || width.empty()) ) {
+
+ input >> token;
+ // std::cout << "|" << token << "|" << std::endl;
+
+ if (regex1->match(token, match_info)) {
+ width = match_info.fetch(1).raw();
+ }
+
+ if (regex2->match(token, match_info)) {
+ height = match_info.fetch(1).raw();
+ }
+
+ }
+ }
+ }
+
+ // TODO: replace int to string conversion with std::to_string when fully C++11 compliant
+ if (height.empty() || width.empty()) {
+ std::ostringstream s_width;
+ std::ostringstream s_height;
+ s_width << imgWidth;
+ s_height << imgHeight;
+ width = s_width.str();
+ height = s_height.str();
+ }
+
+ // Find the minimum scale to fit the image inside the preview area
+ double scaleFactorX = (0.9 * (double)previewWidth) / ((double)imgWidth);
+ double scaleFactorY = (0.9 * (double)previewHeight) / ((double)imgHeight);
+ double scaleFactor = scaleFactorX;
+ if (scaleFactorX > scaleFactorY)
+ scaleFactor = scaleFactorY;
+
+ // Now get the resized values
+ gint scaledImgWidth = (int)(scaleFactor * (double)imgWidth);
+ gint scaledImgHeight = (int)(scaleFactor * (double)imgHeight);
+
+ // center the image on the area
+ gint imgX = (previewWidth - scaledImgWidth) / 2;
+ gint imgY = (previewHeight - scaledImgHeight) / 2;
+
+ // wrap a rectangle around the image
+ gint rectX = imgX - 1;
+ gint rectY = imgY - 1;
+ gint rectWidth = scaledImgWidth + 2;
+ gint rectHeight = scaledImgHeight + 2;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"A(
+<svg width="%d" height="%d"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect width="100%" height="100%" style="fill:#eeeeee"/>
+ <image x="%d" y="%d" width="%d" height="%d" xlink:href="%s"/>
+ <rect x="%d" y="%d" width="%d" height="%d" style="fill:none;stroke:black"/>
+ <text x="50%" y="55%" style="font-family:sans-serif;font-size:24px;text-anchor:middle">%s x %s</text>
+</svg>
+)A";
+
+ // if (!Glib::get_charset()) //If we are not utf8
+ fileName = Glib::filename_to_utf8(fileName);
+ // Filenames in xlinks are decoded, so any % will break without this.
+ auto encodedName = Glib::uri_escape_string(fileName);
+
+ // Fill in the template
+ /* FIXME: Do proper XML quoting for fileName. */
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, previewWidth, previewHeight, imgX, imgY, scaledImgWidth, scaledImgHeight,
+ encodedName.c_str(), rectX, rectY, rectWidth, rectHeight, width.c_str(), height.c_str() );
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+
+
+void SVGPreview::showNoPreview()
+{
+ // Are we already showing it?
+ if (showingNoPreview)
+ return;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"B(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="320"
+ style="font-size:32px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)B";
+
+ // Fill in the template
+ gchar *xmlBuffer = g_strdup_printf(xformat, _("No preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // Now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+ showingNoPreview = true;
+}
+
+
+/**
+ * Inform the user that the svg file is too large to be displayed.
+ * This does not check for sizes of embedded images (yet)
+ */
+void SVGPreview::showTooLarge(long fileLength)
+{
+ // Our template. Modify to taste
+ gchar const *xformat = R"C(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="280"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%.1f MB</text>
+ <text xml:space="preserve" x="200" y="360"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)C";
+
+
+ // Fill in the template
+ double floatFileLength = ((double)fileLength) / 1048576.0;
+ // printf("%ld %f\n", fileLength, floatFileLength);
+
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, floatFileLength, _("Too large for preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+bool SVGPreview::set(Glib::ustring &fileName, int dialogType)
+{
+
+ if (!Glib::file_test(fileName, Glib::FILE_TEST_EXISTS)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)) {
+ Glib::ustring fileNameUtf8 = Glib::filename_to_utf8(fileName);
+ gchar *fName = const_cast<gchar *>(
+ fileNameUtf8.c_str()); // const-cast probably not necessary? (not necessary on Windows version of stat())
+ GStatBuf info;
+ if (g_stat(fName, &info)) // stat returns 0 upon success
+ {
+ g_warning("SVGPreview::set() : %s : %s", fName, strerror(errno));
+ return false;
+ }
+ if (info.st_size > 0xA00000L) {
+ showingNoPreview = false;
+ showTooLarge(info.st_size);
+ return false;
+ }
+ }
+
+ Glib::ustring svg = ".svg";
+ Glib::ustring svgz = ".svgz";
+
+ if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) &&
+ (hasSuffix(fileName, svg) || hasSuffix(fileName, svgz))) {
+ bool retval = setFileName(fileName);
+ showingNoPreview = false;
+ return retval;
+ } else if (isValidImageFile(fileName)) {
+ showImage(fileName);
+ showingNoPreview = false;
+ return true;
+ } else {
+ showNoPreview();
+ return false;
+ }
+}
+
+
+SVGPreview::SVGPreview()
+ : document(nullptr)
+ , viewer(nullptr)
+ , showingNoPreview(false)
+{
+ set_size_request(200, 300);
+}
+
+SVGPreview::~SVGPreview()
+{
+ if (viewer) {
+ viewer->setDocument(nullptr);
+ }
+ delete document;
+}
+
+} // 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/svg-preview.h b/src/ui/dialog/svg-preview.h
new file mode 100644
index 0000000..8263e15
--- /dev/null
+++ b/src/ui/dialog/svg-preview.h
@@ -0,0 +1,123 @@
+// 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 <johan@shouraizou.nl>
+ * 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 __SVG_PREVIEW_H__
+#define __SVG_PREVIEW_H__
+
+//Gtk includes
+#include <gtkmm.h>
+
+//General includes
+#include <unistd.h>
+#include <sys/stat.h>
+#include <cerrno>
+
+#include "filedialog.h"
+
+
+namespace Gtk {
+class Expander;
+}
+
+namespace Inkscape {
+ class URI;
+
+namespace UI {
+
+namespace View {
+ class SVGViewWidget;
+}
+
+namespace Dialog {
+
+/*#########################################################################
+### SVG Preview Widget
+#########################################################################*/
+
+/**
+ * Simple class for displaying an SVG file in the "preview widget."
+ * Currently, this is just a wrapper of the sp_svg_view Gtk widget.
+ * Hopefully we will eventually replace with a pure Gtkmm widget.
+ */
+class SVGPreview : public Gtk::VBox
+{
+public:
+
+ SVGPreview();
+
+ ~SVGPreview() override;
+
+ bool setDocument(SPDocument *doc);
+
+ bool setFileName(Glib::ustring &fileName);
+
+ bool setFromMem(char const *xmlBuffer);
+
+ bool set(Glib::ustring &fileName, int dialogType);
+
+ bool setURI(URI &uri);
+
+ /**
+ * Show image embedded in SVG
+ */
+ void showImage(Glib::ustring &fileName);
+
+ /**
+ * Show the "No preview" image
+ */
+ void showNoPreview();
+
+ /**
+ * Show the "Too large" image
+ */
+ void showTooLarge(long fileLength);
+
+private:
+ /**
+ * The svg document we are currently showing
+ */
+ SPDocument *document;
+
+ /**
+ * The sp_svg_view widget
+ */
+ Inkscape::UI::View::SVGViewWidget *viewer;
+
+ /**
+ * are we currently showing the "no preview" image?
+ */
+ bool showingNoPreview;
+
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /*__SVG_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/swatches.cpp b/src/ui/dialog/swatches.cpp
new file mode 100644
index 0000000..c79b660
--- /dev/null
+++ b/src/ui/dialog/swatches.cpp
@@ -0,0 +1,1418 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ * John Bintz
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2008 John Bintz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <map>
+#include <algorithm>
+#include <iomanip>
+#include <set>
+
+#include "swatches.h"
+#include <gtkmm/radiomenuitem.h>
+
+#include <gtkmm/menu.h>
+#include <gtkmm/checkmenuitem.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/separatormenuitem.h>
+#include <gtkmm/menubutton.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/timer.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/miscutils.h>
+
+#include "color-item.h"
+#include "desktop.h"
+
+#include "desktop-style.h"
+#include "document.h"
+#include "document-undo.h"
+#include "extension/db.h"
+#include "inkscape.h"
+#include "io/sys.h"
+#include "io/resource.h"
+#include "message-context.h"
+#include "path-prefix.h"
+
+#include "ui/previewholder.h"
+#include "widgets/desktop-widget.h"
+#include "widgets/gradient-vector.h"
+#include "display/cairo-utils.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-gradient-reference.h"
+
+#include "dialog-manager.h"
+#include "verbs.h"
+#include "gradient-chemistry.h"
+#include "helper/action.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+enum {
+ SWATCHES_SETTINGS_SIZE = 0,
+ SWATCHES_SETTINGS_MODE = 1,
+ SWATCHES_SETTINGS_SHAPE = 2,
+ SWATCHES_SETTINGS_WRAP = 3,
+ SWATCHES_SETTINGS_BORDER = 4,
+ SWATCHES_SETTINGS_PALETTE = 5
+};
+
+#define VBLOCK 16
+#define PREVIEW_PIXBUF_WIDTH 128
+
+void _loadPaletteFile( gchar const *filename, gboolean user=FALSE );
+
+std::list<SwatchPage*> userSwatchPages;
+std::list<SwatchPage*> systemSwatchPages;
+static std::map<SPDocument*, SwatchPage*> docPalettes;
+static std::vector<DocTrack*> docTrackings;
+static std::map<SwatchesPanel*, SPDocument*> docPerPanel;
+
+
+class SwatchesPanelHook : public SwatchesPanel
+{
+public:
+ static void convertGradient( GtkMenuItem *menuitem, gpointer userData );
+ static void deleteGradient( GtkMenuItem *menuitem, gpointer userData );
+};
+
+static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(callback_data);
+ if ( item ) {
+ item->buttonClicked(false);
+ }
+}
+
+static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(callback_data);
+ if ( item ) {
+ item->buttonClicked(true);
+ }
+}
+
+static GtkWidget* popupMenu = nullptr;
+static GtkWidget *popupSubHolder = nullptr;
+static GtkWidget *popupSub = nullptr;
+static std::vector<Glib::ustring> popupItems;
+static std::vector<GtkWidget*> popupExtras;
+static ColorItem* bounceTarget = nullptr;
+static SwatchesPanel* bouncePanel = nullptr;
+
+static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ handleClick( GTK_WIDGET(menuitem), bounceTarget );
+ }
+}
+
+static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ handleSecondaryClick( GTK_WIDGET(menuitem), 0, bounceTarget );
+ }
+}
+
+static void editGradientImpl( SPDesktop* desktop, SPGradient* gr )
+{
+ if ( gr ) {
+ bool shown = false;
+ if ( desktop && desktop->doc() ) {
+ Inkscape::Selection *selection = desktop->getSelection();
+ std::vector<SPItem*> const items(selection->items().begin(), selection->items().end());
+ if (!items.empty()) {
+ SPStyle query( desktop->doc() );
+ int result = objects_query_fillstroke((items), &query, true);
+ if ( (result == QUERY_STYLE_MULTIPLE_SAME) || (result == QUERY_STYLE_SINGLE) ) {
+ // could be pertinent
+ if (query.fill.isPaintserver()) {
+ SPPaintServer* server = query.getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* grad = SP_GRADIENT(server);
+ if ( grad->isSwatch() && grad->getId() == gr->getId()) {
+ desktop->_dlg_mgr->showDialog("FillAndStroke");
+ shown = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!shown) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) {
+ // Legacy gradient dialog
+ GtkWidget *dialog = sp_gradient_vector_editor_new( gr );
+ gtk_widget_show( dialog );
+ } else {
+ // Invoke the gradient tool
+ Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT );
+ if ( verb ) {
+ SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) );
+ if ( action ) {
+ sp_action_perform( action, nullptr );
+ }
+ }
+ }
+ }
+ }
+}
+
+static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ SPDocument *doc = desktop ? desktop->doc() : nullptr;
+ if (doc) {
+ std::string targetName(bounceTarget->def.descr);
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( targetName == grad->getId() ) {
+ editGradientImpl( desktop, grad );
+ break;
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanelHook::convertGradient( GtkMenuItem * /*menuitem*/, gpointer userData )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ SPDocument *doc = desktop ? desktop->doc() : nullptr;
+ gint index = GPOINTER_TO_INT(userData);
+ if ( doc && (index >= 0) && (static_cast<guint>(index) < popupItems.size()) ) {
+ Glib::ustring targetName = popupItems[index];
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+
+ if ( targetName == grad->getId() ) {
+ grad->setSwatch();
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT,
+ _("Add gradient stop"));
+ break;
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanelHook::deleteGradient( GtkMenuItem */*menuitem*/, gpointer /*userData*/ )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ sp_gradient_unset_swatch(desktop, bounceTarget->def.descr);
+ }
+}
+
+static SwatchesPanel* findContainingPanel( GtkWidget *widget )
+{
+ SwatchesPanel *swp = nullptr;
+
+ std::map<GtkWidget*, SwatchesPanel*> rawObjects;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) {
+ rawObjects[GTK_WIDGET(it->first->gobj())] = it->first;
+ }
+
+ for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) {
+ if (rawObjects.find(curr) != rawObjects.end()) {
+ swp = rawObjects[curr];
+ }
+ }
+
+ return swp;
+}
+
+static void removeit( GtkWidget *widget, gpointer data )
+{
+ gtk_container_remove( GTK_CONTAINER(data), widget );
+}
+
+/* extern'ed from color-item.cpp */
+bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data)
+{
+ gboolean handled = FALSE;
+
+ if ( event && (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) {
+ SwatchesPanel* swp = findContainingPanel( GTK_WIDGET(preview->gobj()) );
+
+ if ( !popupMenu ) {
+ popupMenu = gtk_menu_new();
+ GtkWidget* child = nullptr;
+
+ //TRANSLATORS: An item in context menu on a colour in the swatches
+ child = gtk_menu_item_new_with_label(_("Set fill"));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(redirClick),
+ user_data);
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+
+ //TRANSLATORS: An item in context menu on a colour in the swatches
+ child = gtk_menu_item_new_with_label(_("Set stroke"));
+
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(redirSecondaryClick),
+ user_data);
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+
+ child = gtk_separator_menu_item_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_menu_item_new_with_label(_("Delete"));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(SwatchesPanelHook::deleteGradient),
+ user_data );
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+ gtk_widget_set_sensitive( child, FALSE );
+
+ child = gtk_menu_item_new_with_label(_("Edit..."));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(editGradient),
+ user_data );
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_separator_menu_item_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_menu_item_new_with_label(_("Convert"));
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ //popupExtras.push_back(child);
+ //gtk_widget_set_sensitive( child, FALSE );
+ {
+ popupSubHolder = child;
+ popupSub = gtk_menu_new();
+ gtk_menu_item_set_submenu( GTK_MENU_ITEM(child), popupSub );
+ }
+
+ gtk_widget_show_all(popupMenu);
+ }
+
+ if ( user_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(user_data);
+ bool show = swp && (swp->getSelectedIndex() == 0);
+ for (auto & popupExtra : popupExtras) {
+ gtk_widget_set_sensitive(popupExtra, show);
+ }
+
+ bounceTarget = item;
+ bouncePanel = swp;
+ popupItems.clear();
+ if ( popupMenu ) {
+ gtk_container_foreach(GTK_CONTAINER(popupSub), removeit, popupSub);
+ bool processed = false;
+ GtkWidget *wdgt = gtk_widget_get_ancestor(GTK_WIDGET(preview->gobj()), SP_TYPE_DESKTOP_WIDGET);
+ if ( wdgt ) {
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt);
+ if ( dtw && dtw->desktop ) {
+ // Pick up all gradients with vectors
+ std::vector<SPObject *> gradients = (dtw->desktop->doc())->getResourceList("gradient");
+ gint index = 0;
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && !grad->isSwatch() ) {
+ //gl = g_slist_prepend(gl, curr->data);
+ processed = true;
+ GtkWidget *child = gtk_menu_item_new_with_label(grad->getId());
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupSub), child);
+
+ popupItems.emplace_back(grad->getId());
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(SwatchesPanelHook::convertGradient),
+ GINT_TO_POINTER(index) );
+ index++;
+ }
+ }
+
+ gtk_widget_show_all(popupSub);
+ }
+ }
+ gtk_widget_set_sensitive( popupSubHolder, processed );
+ gtk_menu_popup_at_pointer(GTK_MENU(popupMenu), reinterpret_cast<GdkEvent *>(event));
+ handled = TRUE;
+ }
+ }
+ }
+
+ return handled;
+}
+
+
+static char* trim( char* str ) {
+ char* ret = str;
+ while ( *str && (*str == ' ' || *str == '\t') ) {
+ str++;
+ }
+ ret = str;
+ while ( *str ) {
+ str++;
+ }
+ str--;
+ while ( str >= ret && (( *str == ' ' || *str == '\t' ) || *str == '\r' || *str == '\n') ) {
+ *str-- = 0;
+ }
+ return ret;
+}
+
+static void skipWhitespace( char*& str ) {
+ while ( *str == ' ' || *str == '\t' ) {
+ str++;
+ }
+}
+
+static bool parseNum( char*& str, int& val ) {
+ val = 0;
+ while ( '0' <= *str && *str <= '9' ) {
+ val = val * 10 + (*str - '0');
+ str++;
+ }
+ bool retval = !(*str == 0 || *str == ' ' || *str == '\t' || *str == '\r' || *str == '\n');
+ return retval;
+}
+
+
+void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/)
+{
+ Glib::ustring filename = Glib::path_get_basename(path);
+ char block[1024];
+ FILE *f = Inkscape::IO::fopen_utf8name(path.c_str(), "r");
+ if ( f ) {
+ char* result = fgets( block, sizeof(block), f );
+ if ( result ) {
+ if ( strncmp( "GIMP Palette", block, 12 ) == 0 ) {
+ bool inHeader = true;
+ bool hasErr = false;
+
+ SwatchPage *onceMore = new SwatchPage();
+ onceMore->_name = filename.c_str();
+
+ do {
+ result = fgets( block, sizeof(block), f );
+ block[sizeof(block) - 1] = 0;
+ if ( result ) {
+ if ( block[0] == '#' ) {
+ // ignore comment
+ } else {
+ char *ptr = block;
+ // very simple check for header versus entry
+ while ( *ptr == ' ' || *ptr == '\t' ) {
+ ptr++;
+ }
+ if ( (*ptr == 0) || (*ptr == '\r') || (*ptr == '\n') ) {
+ // blank line. skip it.
+ } else if ( '0' <= *ptr && *ptr <= '9' ) {
+ // should be an entry link
+ inHeader = false;
+ ptr = block;
+ Glib::ustring name("");
+ skipWhitespace(ptr);
+ if ( *ptr ) {
+ int r = 0;
+ int g = 0;
+ int b = 0;
+ hasErr = parseNum(ptr, r);
+ if ( !hasErr ) {
+ skipWhitespace(ptr);
+ hasErr = parseNum(ptr, g);
+ }
+ if ( !hasErr ) {
+ skipWhitespace(ptr);
+ hasErr = parseNum(ptr, b);
+ }
+ if ( !hasErr && *ptr ) {
+ char* n = trim(ptr);
+ if (n != nullptr && *n) {
+ name = g_dpgettext2(nullptr, "Palette", n);
+ }
+ if (name == "") {
+ name = Glib::ustring::compose("#%1%2%3",
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), r),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), g),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), b)
+ ).uppercase();
+ }
+ }
+ if ( !hasErr ) {
+ // Add the entry now
+ Glib::ustring nameStr(name);
+ ColorItem* item = new ColorItem( r, g, b, nameStr );
+ onceMore->_colors.push_back(item);
+ }
+ } else {
+ hasErr = true;
+ }
+ } else {
+ if ( !inHeader ) {
+ // Hmmm... probably bad. Not quite the format we want?
+ hasErr = true;
+ } else {
+ char* sep = strchr(result, ':');
+ if ( sep ) {
+ *sep = 0;
+ char* val = trim(sep + 1);
+ char* name = trim(result);
+ if ( *name ) {
+ if ( strcmp( "Name", name ) == 0 )
+ {
+ onceMore->_name = val;
+ }
+ else if ( strcmp( "Columns", name ) == 0 )
+ {
+ gchar* endPtr = nullptr;
+ guint64 numVal = g_ascii_strtoull( val, &endPtr, 10 );
+ if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) {
+ // overflow
+ } else if ( (numVal == 0) && (endPtr == val) ) {
+ // failed conversion
+ } else {
+ onceMore->_prefWidth = numVal;
+ }
+ }
+ } else {
+ // error
+ hasErr = true;
+ }
+ } else {
+ // error
+ hasErr = true;
+ }
+ }
+ }
+ }
+ }
+ } while ( result && !hasErr );
+ if ( !hasErr ) {
+ if (user)
+ userSwatchPages.push_back(onceMore);
+ else
+ systemSwatchPages.push_back(onceMore);
+#if ENABLE_MAGIC_COLORS
+ ColorItem::_wireMagicColors( onceMore );
+#endif // ENABLE_MAGIC_COLORS
+ } else {
+ delete onceMore;
+ }
+ }
+ }
+
+ fclose(f);
+ }
+}
+
+static bool
+compare_swatch_names(SwatchPage const *a, SwatchPage const *b) {
+
+ return g_utf8_collate(a->_name.c_str(), b->_name.c_str()) < 0;
+}
+
+static void load_palettes()
+{
+ static bool init_done = false;
+
+ if (init_done) {
+ return;
+ }
+ init_done = true;
+
+ for (auto &filename: Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) {
+ bool userPalette = Inkscape::IO::file_is_writable(filename.c_str());
+ _loadPaletteFile(filename, userPalette);
+ }
+
+ // Sort the list of swatches by name, grouped by user/system
+ userSwatchPages.sort(compare_swatch_names);
+ systemSwatchPages.sort(compare_swatch_names);
+}
+
+SwatchesPanel& SwatchesPanel::getInstance()
+{
+ return *new SwatchesPanel();
+}
+
+
+/**
+ * Constructor
+ */
+SwatchesPanel::SwatchesPanel(gchar const* prefsPath) :
+ Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SWATCHES),
+ _menu(nullptr),
+ _holder(nullptr),
+ _clear(nullptr),
+ _remove(nullptr),
+ _currentIndex(0),
+ _currentDesktop(nullptr),
+ _currentDocument(nullptr)
+{
+ _holder = new PreviewHolder();
+
+ _build_menu();
+
+ auto menu_button = Gtk::manage(new Gtk::MenuButton());
+ menu_button->set_halign(Gtk::ALIGN_END);
+ menu_button->set_relief(Gtk::RELIEF_NONE);
+ menu_button->set_image_from_icon_name("pan-start-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ menu_button->set_popup(*_menu);
+
+ auto box = Gtk::manage(new Gtk::Box());
+
+ if (_prefs_path == "/dialogs/swatches") {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ box->pack_start(*menu_button, Gtk::PACK_SHRINK);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ box->pack_end(*menu_button, Gtk::PACK_SHRINK);
+ _updateSettings(SWATCHES_SETTINGS_MODE, 1);
+ _holder->setOrientation(SP_ANCHOR_SOUTH);
+ }
+
+ box->pack_start(*_holder, Gtk::PACK_EXPAND_WIDGET);
+ _getContents()->pack_start(*box);
+
+ load_palettes();
+
+ Gtk::RadioMenuItem* hotItem = nullptr;
+ _clear = new ColorItem( ege::PaintDef::CLEAR );
+ _remove = new ColorItem( ege::PaintDef::NONE );
+
+ if (docPalettes.empty()) {
+ SwatchPage *docPalette = new SwatchPage();
+
+ docPalette->_name = "Auto";
+ docPalettes[nullptr] = docPalette;
+ }
+
+ if ( !systemSwatchPages.empty() || !userSwatchPages.empty()) {
+ SwatchPage* first = nullptr;
+ int index = 0;
+ Glib::ustring targetName;
+ if ( !_prefs_path.empty() ) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ targetName = prefs->getString(_prefs_path + "/palette");
+ if (!targetName.empty()) {
+ if (targetName == "Auto") {
+ first = docPalettes[nullptr];
+ } else {
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ for (auto & page : pages) {
+ if ( page->_name == targetName ) {
+ first = page;
+ break;
+ }
+ index++;
+ }
+ }
+ }
+ }
+
+ if ( !first ) {
+ first = docPalettes[nullptr];
+ _currentIndex = 0;
+ } else {
+ _currentIndex = index;
+ }
+
+ _rebuild();
+
+ Gtk::RadioMenuItem::Group groupOne;
+
+ int i = 0;
+ std::vector<SwatchPage*> swatchSets = _getSwatchSets();
+ for (auto curr : swatchSets) {
+ Gtk::RadioMenuItem* single = Gtk::manage(new Gtk::RadioMenuItem(groupOne, curr->_name));
+ if ( curr == first ) {
+ hotItem = single;
+ }
+ _regItem(single, i);
+
+ i++;
+ }
+ }
+
+ if ( hotItem ) {
+ hotItem->set_active();
+ }
+
+ show_all_children();
+}
+
+SwatchesPanel::~SwatchesPanel()
+{
+ _trackDocument( this, nullptr );
+
+ _documentConnection.disconnect();
+ _selChanged.disconnect();
+
+ if ( _clear ) {
+ delete _clear;
+ }
+ if ( _remove ) {
+ delete _remove;
+ }
+ if ( _holder ) {
+ delete _holder;
+ }
+
+ delete _menu;
+}
+
+void SwatchesPanel::_build_menu()
+{
+ guint panel_size = 0, panel_mode = 0, panel_ratio = 100, panel_border = 0;
+ bool panel_wrap = false;
+ if (!_prefs_path.empty()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ panel_wrap = prefs->getBool(_prefs_path + "/panel_wrap");
+ panel_size = prefs->getIntLimited(_prefs_path + "/panel_size", 1, 0, UI::Widget::PREVIEW_SIZE_HUGE);
+ panel_mode = prefs->getIntLimited(_prefs_path + "/panel_mode", 1, 0, 10);
+ panel_ratio = prefs->getIntLimited(_prefs_path + "/panel_ratio", 100, 0, 500 );
+ panel_border = prefs->getIntLimited(_prefs_path + "/panel_border", UI::Widget::BORDER_NONE, 0, 2 );
+ }
+
+ _menu = new Gtk::Menu();
+
+ if (_prefs_path == "/dialogs/swatches") {
+ Gtk::RadioMenuItem::Group group;
+ Glib::ustring list_label(_("List"));
+ Glib::ustring grid_label(_("Grid"));
+ Gtk::RadioMenuItem *list_item = Gtk::manage(new Gtk::RadioMenuItem(group, list_label));
+ Gtk::RadioMenuItem *grid_item = Gtk::manage(new Gtk::RadioMenuItem(group, grid_label));
+
+ if (panel_mode == 0) {
+ list_item->set_active(true);
+ } else if (panel_mode == 1) {
+ grid_item->set_active(true);
+ }
+
+ _menu->append(*list_item);
+ _menu->append(*grid_item);
+ _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
+
+ list_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 0));
+ grid_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 1));
+ }
+
+ {
+ Glib::ustring heightItemLabel(C_("Swatches", "Size"));
+
+ //TRANSLATORS: Indicates size of colour swatches
+ const gchar *heightLabels[] = {
+ NC_("Swatches height", "Tiny"),
+ NC_("Swatches height", "Small"),
+ NC_("Swatches height", "Medium"),
+ NC_("Swatches height", "Large"),
+ NC_("Swatches height", "Huge")
+ };
+
+ Gtk::MenuItem *sizeItem = Gtk::manage(new Gtk::MenuItem(heightItemLabel));
+ Gtk::Menu *sizeMenu = Gtk::manage(new Gtk::Menu());
+ sizeItem->set_submenu(*sizeMenu);
+
+ Gtk::RadioMenuItem::Group heightGroup;
+ for (unsigned int i = 0; i < G_N_ELEMENTS(heightLabels); i++) {
+ Glib::ustring _label(g_dpgettext2(nullptr, "Swatches height", heightLabels[i]));
+ Gtk::RadioMenuItem* _item = Gtk::manage(new Gtk::RadioMenuItem(heightGroup, _label));
+ sizeMenu->append(*_item);
+ if (i == panel_size) {
+ _item->set_active(true);
+ }
+ _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SIZE, i));
+ }
+
+ _menu->append(*sizeItem);
+ }
+
+ {
+ Glib::ustring widthItemLabel(C_("Swatches", "Width"));
+
+ //TRANSLATORS: Indicates width of colour swatches
+ const gchar *widthLabels[] = {
+ NC_("Swatches width", "Narrower"),
+ NC_("Swatches width", "Narrow"),
+ NC_("Swatches width", "Medium"),
+ NC_("Swatches width", "Wide"),
+ NC_("Swatches width", "Wider")
+ };
+
+ Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel));
+ Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu());
+ item->set_submenu(*type_menu);
+ _menu->append(*item);
+
+ Gtk::RadioMenuItem::Group widthGroup;
+
+ guint values[] = {0, 25, 50, 100, 200, 400};
+ guint hot_index = 3;
+ for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) {
+ // Assume all values are in increasing order
+ if ( values[i] <= panel_ratio ) {
+ hot_index = i;
+ }
+ }
+ for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) {
+ Glib::ustring _label(g_dpgettext2(nullptr, "Swatches width", widthLabels[i]));
+ Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label));
+ type_menu->append(*_item);
+ if ( i <= hot_index ) {
+ _item->set_active(true);
+ }
+ _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SHAPE, values[i]));
+ }
+ }
+
+ {
+ Glib::ustring widthItemLabel(C_("Swatches", "Border"));
+
+ //TRANSLATORS: Indicates border of colour swatches
+ const gchar *widthLabels[] = {
+ NC_("Swatches border", "None"),
+ NC_("Swatches border", "Solid"),
+ NC_("Swatches border", "Wide"),
+ };
+
+ Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel));
+ Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu());
+ item->set_submenu(*type_menu);
+ _menu->append(*item);
+
+ Gtk::RadioMenuItem::Group widthGroup;
+
+ guint values[] = {0, 1, 2};
+ guint hot_index = 0;
+ for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) {
+ // Assume all values are in increasing order
+ if ( values[i] <= panel_border ) {
+ hot_index = i;
+ }
+ }
+ for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) {
+ Glib::ustring _label(g_dpgettext2(nullptr, "Swatches border", widthLabels[i]));
+ Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label));
+ type_menu->append(*_item);
+ if ( i <= hot_index ) {
+ _item->set_active(true);
+ }
+ _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_BORDER, values[i]));
+ }
+ }
+
+ if (_prefs_path == "/embedded/swatches") {
+ //TRANSLATORS: "Wrap" indicates how colour swatches are displayed
+ Glib::ustring wrap_label(C_("Swatches","Wrap"));
+ Gtk::CheckMenuItem *check = Gtk::manage(new Gtk::CheckMenuItem(wrap_label));
+ check->set_active(panel_wrap);
+ _menu->append(*check);
+
+ check->signal_toggled().connect(sigc::bind<Gtk::CheckMenuItem*>(sigc::mem_fun(*this, &SwatchesPanel::_wrapToggled), check));
+ }
+
+ _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
+
+ _menu->show_all();
+
+ _updateSettings(SWATCHES_SETTINGS_SIZE, panel_size);
+ _updateSettings(SWATCHES_SETTINGS_MODE, panel_mode);
+ _updateSettings(SWATCHES_SETTINGS_SHAPE, panel_ratio);
+ _updateSettings(SWATCHES_SETTINGS_WRAP, panel_wrap);
+ _updateSettings(SWATCHES_SETTINGS_BORDER, panel_border);
+}
+
+void SwatchesPanel::setDesktop( SPDesktop* desktop )
+{
+ if ( desktop != _currentDesktop ) {
+ if ( _currentDesktop ) {
+ _documentConnection.disconnect();
+ _selChanged.disconnect();
+ }
+
+ _currentDesktop = desktop;
+
+ if ( desktop ) {
+ _currentDesktop->selection->connectChanged(
+ sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection)));
+
+ _currentDesktop->selection->connectModified(
+ sigc::hide(sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))));
+
+ _currentDesktop->connectToolSubselectionChanged(
+ sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection)));
+
+ sigc::bound_mem_functor1<void, SwatchesPanel, SPDocument*> first = sigc::mem_fun(*this, &SwatchesPanel::_setDocument);
+ sigc::slot<void, SPDocument*> base2 = first;
+ sigc::slot<void,SPDesktop*, SPDocument*> slot2 = sigc::hide<0>( base2 );
+ _documentConnection = desktop->connectDocumentReplaced( slot2 );
+
+ _setDocument( desktop->doc() );
+ } else {
+ _setDocument(nullptr);
+ }
+ }
+}
+
+
+void SwatchesPanel::_updateSettings(int settings, int value)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ switch (settings) {
+ case SWATCHES_SETTINGS_SIZE: {
+ prefs->setInt(_prefs_path + "/panel_size", value);
+
+ auto curr_type = _holder->getPreviewType();
+ guint curr_ratio = _holder->getPreviewRatio();
+ auto curr_border = _holder->getPreviewBorder();
+
+ switch (value) {
+ case 0:
+ _holder->setStyle(UI::Widget::PREVIEW_SIZE_TINY, curr_type, curr_ratio, curr_border);
+ break;
+ case 1:
+ _holder->setStyle(UI::Widget::PREVIEW_SIZE_SMALL, curr_type, curr_ratio, curr_border);
+ break;
+ case 2:
+ _holder->setStyle(UI::Widget::PREVIEW_SIZE_MEDIUM, curr_type, curr_ratio, curr_border);
+ break;
+ case 3:
+ _holder->setStyle(UI::Widget::PREVIEW_SIZE_BIG, curr_type, curr_ratio, curr_border);
+ break;
+ case 4:
+ _holder->setStyle(UI::Widget::PREVIEW_SIZE_HUGE, curr_type, curr_ratio, curr_border);
+ break;
+ default:
+ break;
+ }
+
+ break;
+ }
+ case SWATCHES_SETTINGS_MODE: {
+ prefs->setInt(_prefs_path + "/panel_mode", value);
+
+ auto curr_size = _holder->getPreviewSize();
+ guint curr_ratio = _holder->getPreviewRatio();
+ auto curr_border = _holder->getPreviewBorder();
+ switch (value) {
+ case 0:
+ _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_LIST, curr_ratio, curr_border);
+ break;
+ case 1:
+ _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_GRID, curr_ratio, curr_border);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case SWATCHES_SETTINGS_SHAPE: {
+ prefs->setInt(_prefs_path + "/panel_ratio", value);
+
+ auto curr_type = _holder->getPreviewType();
+ auto curr_size = _holder->getPreviewSize();
+ auto curr_border = _holder->getPreviewBorder();
+
+ _holder->setStyle(curr_size, curr_type, value, curr_border);
+ break;
+ }
+ case SWATCHES_SETTINGS_BORDER: {
+ prefs->setInt(_prefs_path + "/panel_border", value);
+
+ auto curr_size = _holder->getPreviewSize();
+ auto curr_type = _holder->getPreviewType();
+ guint curr_ratio = _holder->getPreviewRatio();
+
+ switch (value) {
+ case 0:
+ _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_NONE);
+ break;
+ case 1:
+ _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_SOLID);
+ break;
+ case 2:
+ _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_WIDE);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case SWATCHES_SETTINGS_WRAP: {
+ prefs->setBool(_prefs_path + "/panel_wrap", value);
+ _holder->setWrap(value);
+ break;
+ }
+ case SWATCHES_SETTINGS_PALETTE: {
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ if (value >= 0 && value < static_cast<int>(pages.size()) ) {
+ _currentIndex = value;
+
+ prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name);
+
+ _rebuild();
+ }
+ }
+ default:
+ break;
+ }
+}
+
+void SwatchesPanel::_wrapToggled(Gtk::CheckMenuItem* toggler)
+{
+ if (toggler) {
+ _updateSettings(SWATCHES_SETTINGS_WRAP, toggler->get_active() ? 1 : 0);
+ }
+}
+
+void SwatchesPanel::_regItem(Gtk::MenuItem* item, int id)
+{
+ _menu->append(*item);
+ item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_PALETTE, id));
+ item->show();
+}
+
+
+class DocTrack
+{
+public:
+ DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) :
+ doc(doc->doRef()),
+ updatePending(false),
+ lastGradientUpdate(0.0),
+ gradientRsrcChanged(gradientRsrcChanged),
+ defsChanged(defsChanged),
+ defsModified(defsModified)
+ {
+ if ( !timer ) {
+ timer = new Glib::Timer();
+ refreshTimer = Glib::signal_timeout().connect( sigc::ptr_fun(handleTimerCB), 33 );
+ }
+ timerRefCount++;
+ }
+
+ ~DocTrack()
+ {
+ timerRefCount--;
+ if ( timerRefCount <= 0 ) {
+ refreshTimer.disconnect();
+ timerRefCount = 0;
+ if ( timer ) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ }
+ if (doc) {
+ gradientRsrcChanged.disconnect();
+ defsChanged.disconnect();
+ defsModified.disconnect();
+ doc->doUnref();
+ doc = nullptr;
+ }
+ }
+
+ static bool handleTimerCB();
+
+ /**
+ * Checks if update should be queued or executed immediately.
+ *
+ * @return true if the update was queued and should not be immediately executed.
+ */
+ static bool queueUpdateIfNeeded(SPDocument *doc);
+
+ static Glib::Timer *timer;
+ static int timerRefCount;
+ static sigc::connection refreshTimer;
+
+ SPDocument *doc;
+ bool updatePending;
+ double lastGradientUpdate;
+ sigc::connection gradientRsrcChanged;
+ sigc::connection defsChanged;
+ sigc::connection defsModified;
+
+private:
+ DocTrack(DocTrack const &) = delete; // no copy
+ DocTrack &operator=(DocTrack const &) = delete; // no assign
+};
+
+Glib::Timer *DocTrack::timer = nullptr;
+int DocTrack::timerRefCount = 0;
+sigc::connection DocTrack::refreshTimer;
+
+static const double DOC_UPDATE_THREASHOLD = 0.090;
+
+bool DocTrack::handleTimerCB()
+{
+ double now = timer->elapsed();
+
+ std::vector<DocTrack *> needCallback;
+ for (auto track : docTrackings) {
+ if ( track->updatePending && ( (now - track->lastGradientUpdate) >= DOC_UPDATE_THREASHOLD) ) {
+ needCallback.push_back(track);
+ }
+ }
+
+ for (auto track : needCallback) {
+ if ( std::find(docTrackings.begin(), docTrackings.end(), track) != docTrackings.end() ) { // Just in case one gets deleted while we are looping
+ // Note: calling handleDefsModified will call queueUpdateIfNeeded and thus update the time and flag.
+ SwatchesPanel::handleDefsModified(track->doc);
+ }
+ }
+
+ return true;
+}
+
+bool DocTrack::queueUpdateIfNeeded( SPDocument *doc )
+{
+ bool deferProcessing = false;
+ for (auto track : docTrackings) {
+ if ( track->doc == doc ) {
+ double now = timer->elapsed();
+ double elapsed = now - track->lastGradientUpdate;
+
+ if ( elapsed < DOC_UPDATE_THREASHOLD ) {
+ deferProcessing = true;
+ track->updatePending = true;
+ } else {
+ track->lastGradientUpdate = now;
+ track->updatePending = false;
+ }
+
+ break;
+ }
+ }
+ return deferProcessing;
+}
+
+void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document )
+{
+ SPDocument *oldDoc = nullptr;
+ if (docPerPanel.find(panel) != docPerPanel.end()) {
+ oldDoc = docPerPanel[panel];
+ if (!oldDoc) {
+ docPerPanel.erase(panel); // Should not be needed, but clean up just in case.
+ }
+ }
+ if (oldDoc != document) {
+ if (oldDoc) {
+ docPerPanel[panel] = nullptr;
+ bool found = false;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) {
+ found = (it->second == document);
+ }
+ if (!found) {
+ for (std::vector<DocTrack*>::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){
+ if ((*it)->doc == oldDoc) {
+ delete *it;
+ docTrackings.erase(it);
+ break;
+ }
+ }
+ }
+ }
+
+ if (document) {
+ bool found = false;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) {
+ found = (it->second == document);
+ }
+ docPerPanel[panel] = document;
+ if (!found) {
+ sigc::connection conn1 = document->connectResourcesChanged( "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) );
+ sigc::connection conn2 = document->getDefs()->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) );
+ sigc::connection conn3 = document->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) );
+
+ DocTrack *dt = new DocTrack(document, conn1, conn2, conn3);
+ docTrackings.push_back(dt);
+
+ if (docPalettes.find(document) == docPalettes.end()) {
+ SwatchPage *docPalette = new SwatchPage();
+ docPalette->_name = "Auto";
+ docPalettes[document] = docPalette;
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanel::_setDocument( SPDocument *document )
+{
+ if ( document != _currentDocument ) {
+ _trackDocument(this, document);
+ _currentDocument = document;
+ handleGradientsChange( document );
+ }
+}
+
+static void recalcSwatchContents(SPDocument* doc,
+ boost::ptr_vector<ColorItem> &tmpColors,
+ std::map<ColorItem*, cairo_pattern_t*> &previewMappings,
+ std::map<ColorItem*, SPGradient*> &gradMappings)
+{
+ std::vector<SPGradient*> newList;
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->isSwatch() ) {
+ newList.push_back(SP_GRADIENT(gradient));
+ }
+ }
+
+ if ( !newList.empty() ) {
+ std::reverse(newList.begin(), newList.end());
+ for (auto grad : newList)
+ {
+ cairo_surface_t *preview = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+ PREVIEW_PIXBUF_WIDTH, VBLOCK);
+ cairo_t *ct = cairo_create(preview);
+
+ Glib::ustring name( grad->getId() );
+ ColorItem* item = new ColorItem( 0, 0, 0, name );
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_pattern_t *gradient = grad->create_preview_pattern(PREVIEW_PIXBUF_WIDTH);
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_set_source(ct, gradient);
+ cairo_paint(ct);
+
+ cairo_destroy(ct);
+ cairo_pattern_destroy(gradient);
+ cairo_pattern_destroy(check);
+
+ cairo_pattern_t *prevpat = cairo_pattern_create_for_surface(preview);
+ cairo_surface_destroy(preview);
+
+ previewMappings[item] = prevpat;
+
+ tmpColors.push_back(item);
+ gradMappings[item] = grad;
+ }
+ }
+}
+
+void SwatchesPanel::handleGradientsChange(SPDocument *document)
+{
+ SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr;
+ if (docPalette) {
+ boost::ptr_vector<ColorItem> tmpColors;
+ std::map<ColorItem*, cairo_pattern_t*> tmpPrevs;
+ std::map<ColorItem*, SPGradient*> tmpGrads;
+ recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads);
+
+ for (auto & tmpPrev : tmpPrevs) {
+ tmpPrev.first->setPattern(tmpPrev.second);
+ cairo_pattern_destroy(tmpPrev.second);
+ }
+
+ for (auto & tmpGrad : tmpGrads) {
+ tmpGrad.first->setGradient(tmpGrad.second);
+ }
+
+ docPalette->_colors.swap(tmpColors);
+
+ // Figure out which SwatchesPanel instances are affected and update them.
+
+ for (auto & it : docPerPanel) {
+ if (it.second == document) {
+ SwatchesPanel* swp = it.first;
+ std::vector<SwatchPage*> pages = swp->_getSwatchSets();
+ SwatchPage* curr = pages[swp->_currentIndex];
+ if (curr == docPalette) {
+ swp->_rebuild();
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanel::handleDefsModified(SPDocument *document)
+{
+ SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr;
+ if (docPalette && !DocTrack::queueUpdateIfNeeded(document) ) {
+ boost::ptr_vector<ColorItem> tmpColors;
+ std::map<ColorItem*, cairo_pattern_t*> tmpPrevs;
+ std::map<ColorItem*, SPGradient*> tmpGrads;
+ recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads);
+
+ if ( tmpColors.size() != docPalette->_colors.size() ) {
+ handleGradientsChange(document);
+ } else {
+ int cap = std::min(docPalette->_colors.size(), tmpColors.size());
+ for (int i = 0; i < cap; i++) {
+ ColorItem *newColor = &tmpColors[i];
+ ColorItem *oldColor = &docPalette->_colors[i];
+ if ( (newColor->def.getType() != oldColor->def.getType()) ||
+ (newColor->def.getR() != oldColor->def.getR()) ||
+ (newColor->def.getG() != oldColor->def.getG()) ||
+ (newColor->def.getB() != oldColor->def.getB()) ) {
+ oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB());
+ }
+ if (tmpGrads.find(newColor) != tmpGrads.end()) {
+ oldColor->setGradient(tmpGrads[newColor]);
+ }
+ if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) {
+ oldColor->setPattern(tmpPrevs[newColor]);
+ }
+ }
+ }
+
+ for (auto & tmpPrev : tmpPrevs) {
+ cairo_pattern_destroy(tmpPrev.second);
+ }
+ }
+}
+
+
+std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const
+{
+ std::vector<SwatchPage*> tmp;
+ if (docPalettes.find(_currentDocument) != docPalettes.end()) {
+ tmp.push_back(docPalettes[_currentDocument]);
+ }
+
+ tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end());
+ tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end());
+
+ return tmp;
+}
+
+void SwatchesPanel::_updateFromSelection()
+{
+ SwatchPage *docPalette = (docPalettes.find(_currentDocument) != docPalettes.end()) ? docPalettes[_currentDocument] : nullptr;
+ if ( docPalette ) {
+ Glib::ustring fillId;
+ Glib::ustring strokeId;
+
+ SPStyle tmpStyle(_currentDesktop->getDocument());
+ int result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_FILL );
+ switch (result) {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ if (tmpStyle.fill.set && tmpStyle.fill.isPaintserver()) {
+ SPPaintServer* server = tmpStyle.getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* target = nullptr;
+ SPGradient* grad = SP_GRADIENT(server);
+
+ if ( grad->isSwatch() ) {
+ target = grad;
+ } else if ( grad->ref ) {
+ SPGradient *tmp = grad->ref->getObject();
+ if ( tmp && tmp->isSwatch() ) {
+ target = tmp;
+ }
+ }
+ if ( target ) {
+ //XML Tree being used directly here while it shouldn't be
+ gchar const* id = target->getRepr()->attribute("id");
+ if ( id ) {
+ fillId = id;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_STROKE );
+ switch (result) {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ if (tmpStyle.stroke.set && tmpStyle.stroke.isPaintserver()) {
+ SPPaintServer* server = tmpStyle.getStrokePaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* target = nullptr;
+ SPGradient* grad = SP_GRADIENT(server);
+ if ( grad->isSwatch() ) {
+ target = grad;
+ } else if ( grad->ref ) {
+ SPGradient *tmp = grad->ref->getObject();
+ if ( tmp && tmp->isSwatch() ) {
+ target = tmp;
+ }
+ }
+ if ( target ) {
+ //XML Tree being used directly here while it shouldn't be
+ gchar const* id = target->getRepr()->attribute("id");
+ if ( id ) {
+ strokeId = id;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ for (auto & _color : docPalette->_colors) {
+ ColorItem* item = &_color;
+ bool isFill = (fillId == item->def.descr);
+ bool isStroke = (strokeId == item->def.descr);
+ item->setState( isFill, isStroke );
+ }
+ }
+}
+
+void SwatchesPanel::_rebuild()
+{
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ SwatchPage* curr = pages[_currentIndex];
+ _holder->clear();
+
+ if ( curr->_prefWidth > 0 ) {
+ _holder->setColumnPref( curr->_prefWidth );
+ }
+ _holder->freezeUpdates();
+ // TODO restore once 'clear' works _holder->addPreview(_clear);
+ _holder->addPreview(_remove);
+ for (auto & _color : curr->_colors) {
+ _holder->addPreview(&_color);
+ }
+ _holder->thawUpdates();
+}
+
+} //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/swatches.h b/src/ui/dialog/swatches.h
new file mode 100644
index 0000000..da10911
--- /dev/null
+++ b/src/ui/dialog/swatches.h
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color swatches dialog
+ */
+/* Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_DIALOGS_SWATCHES_H
+#define SEEN_DIALOGS_SWATCHES_H
+
+#include "ui/widget/panel.h"
+
+namespace Gtk {
+ class Menu;
+ class MenuItem;
+ class CheckMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+
+class PreviewHolder;
+
+namespace Dialog {
+
+class ColorItem;
+class SwatchPage;
+class DocTrack;
+
+/**
+ * A panel that displays paint swatches.
+ *
+ * It comes in two flavors, depending on the prefsPath argument passed to
+ * the constructor: the default "/dialog/swatches" is just a regular panel;
+ * the "/embedded/swatches/" is the horizontal color swatches at the bottom
+ * of window.
+ */
+class SwatchesPanel : public Inkscape::UI::Widget::Panel
+{
+public:
+ SwatchesPanel(gchar const* prefsPath = "/dialogs/swatches");
+ ~SwatchesPanel() override;
+
+ static SwatchesPanel& getInstance();
+
+ void setDesktop( SPDesktop* desktop ) override;
+ virtual SPDesktop* getDesktop() {return _currentDesktop;}
+
+ virtual int getSelectedIndex() {return _currentIndex;} // temporary
+
+protected:
+ static void handleGradientsChange(SPDocument *document);
+
+ virtual void _updateFromSelection();
+ virtual void _setDocument( SPDocument *document );
+ virtual void _rebuild();
+
+ virtual std::vector<SwatchPage*> _getSwatchSets() const;
+
+private:
+ SwatchesPanel(SwatchesPanel const &) = delete; // no copy
+ SwatchesPanel &operator=(SwatchesPanel const &) = delete; // no assign
+
+ void _build_menu();
+
+ static void _trackDocument( SwatchesPanel *panel, SPDocument *document );
+ static void handleDefsModified(SPDocument *document);
+
+ PreviewHolder* _holder;
+ ColorItem* _clear;
+ ColorItem* _remove;
+ int _currentIndex;
+ SPDesktop* _currentDesktop;
+ SPDocument* _currentDocument;
+
+ void _regItem(Gtk::MenuItem* item, int id);
+
+ void _updateSettings(int settings, int value);
+
+ void _wrapToggled(Gtk::CheckMenuItem *toggler);
+
+ Gtk::Menu *_menu;
+
+ sigc::connection _documentConnection;
+ sigc::connection _selChanged;
+
+ friend class DocTrack;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+
+#endif // SEEN_SWATCHES_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/symbols.cpp b/src/ui/dialog/symbols.cpp
new file mode 100644
index 0000000..b03bd5d
--- /dev/null
+++ b/src/ui/dialog/symbols.cpp
@@ -0,0 +1,1403 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Symbols dialog.
+ */
+/* Authors:
+ * Copyright (C) 2012 Tavmjong Bah
+ *
+ * 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 <iostream>
+#include <algorithm>
+#include <locale>
+#include <sstream>
+#include <fstream>
+#include <regex>
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/regex.h>
+#include <glibmm/stringutils.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "selection.h"
+#include "symbols.h"
+#include "verbs.h"
+
+#include "display/cairo-utils.h"
+#include "helper/action.h"
+#include "include/gtkmm_version.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "object/sp-symbol.h"
+#include "object/sp-use.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/clipboard.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#ifdef WITH_LIBVISIO
+ #include <libvisio/libvisio.h>
+
+ // TODO: Drop this check when librevenge is widespread.
+ #if WITH_LIBVISIO01
+ #include <librevenge-stream/librevenge-stream.h>
+
+ using librevenge::RVNGFileStream;
+ using librevenge::RVNGString;
+ using librevenge::RVNGStringVector;
+ using librevenge::RVNGPropertyList;
+ using librevenge::RVNGSVGDrawingGenerator;
+ #else
+ #include <libwpd-stream/libwpd-stream.h>
+
+ typedef WPXFileStream RVNGFileStream;
+ typedef libvisio::VSDStringVector RVNGStringVector;
+ #endif
+#endif
+
+
+namespace Inkscape {
+namespace UI {
+
+namespace Dialog {
+
+// See: http://developer.gnome.org/gtkmm/stable/classGtk_1_1TreeModelColumnRecord.html
+class SymbolColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+
+ Gtk::TreeModelColumn<Glib::ustring> symbol_id;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_doc_title;
+ Gtk::TreeModelColumn< Glib::RefPtr<Gdk::Pixbuf> > symbol_image;
+
+
+ SymbolColumns() {
+ add(symbol_id);
+ add(symbol_title);
+ add(symbol_doc_title);
+ add(symbol_image);
+ }
+};
+
+SymbolColumns* SymbolsDialog::getColumns()
+{
+ SymbolColumns* columns = new SymbolColumns();
+ return columns;
+}
+
+/**
+ * Constructor
+ */
+SymbolsDialog::SymbolsDialog( gchar const* prefsPath ) :
+ UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SYMBOLS),
+ store(Gtk::ListStore::create(*getColumns())),
+ all_docs_processed(false),
+ icon_view(nullptr),
+ current_desktop(nullptr),
+ desk_track(),
+ current_document(nullptr),
+ preview_document(nullptr),
+ instanceConns(),
+ CURRENTDOC(_("Current document")),
+ ALLDOCS(_("All symbol sets"))
+{
+
+ /******************** Table *************************/
+ auto table = new Gtk::Grid();
+
+ table->set_margin_start(3);
+ table->set_margin_end(3);
+ table->set_margin_top(4);
+ // panel is a locked Gtk::VBox
+ _getContents()->pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET);
+ guint row = 0;
+
+ /******************** Symbol Sets *************************/
+ Gtk::Label* label_set = new Gtk::Label(Glib::ustring(_("Symbol set")) + ": ");
+ table->attach(*Gtk::manage(label_set),0,row,1,1);
+ symbol_set = new Gtk::ComboBoxText(); // Fill in later
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ symbol_set->set_active_text(CURRENTDOC);
+ symbol_set->set_hexpand();
+
+ table->attach(*Gtk::manage(symbol_set),1,row,1,1);
+ sigc::connection connSet = symbol_set->signal_changed().connect(
+ sigc::mem_fun(*this, &SymbolsDialog::rebuild));
+ instanceConns.push_back(connSet);
+
+ ++row;
+
+ /******************** Separator *************************/
+
+
+ Gtk::Separator* separator = Gtk::manage(new Gtk::Separator()); // Search
+ separator->set_margin_top(10);
+ separator->set_margin_bottom(10);
+ table->attach(*Gtk::manage(separator),0,row,2,1);
+
+ ++row;
+
+ /******************** Search *************************/
+
+
+ search = Gtk::manage(new Gtk::SearchEntry()); // Search
+ search->set_tooltip_text(_("Return to start search."));
+ search->signal_key_press_event().connect_notify( sigc::mem_fun(*this, &SymbolsDialog::beforeSearch));
+ search->signal_key_release_event().connect_notify(sigc::mem_fun(*this, &SymbolsDialog::unsensitive));
+
+ search->set_margin_bottom(6);
+ search->signal_search_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::clearSearch));
+ table->attach(*Gtk::manage(search),0,row,2,1);
+ search_str = "";
+
+ ++row;
+
+
+ /********************* Icon View **************************/
+ SymbolColumns* columns = getColumns();
+
+ icon_view = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store));
+ //icon_view->set_text_column( columns->symbol_id );
+ icon_view->set_tooltip_column( 1 );
+ icon_view->set_pixbuf_column( columns->symbol_image );
+ // Giving the iconview a small minimum size will help users understand
+ // What the dialog does.
+ icon_view->set_size_request( 100, 250 );
+
+ std::vector< Gtk::TargetEntry > targets;
+ targets.emplace_back( "application/x-inkscape-paste");
+
+ icon_view->enable_model_drag_source (targets, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY);
+ icon_view->signal_drag_data_get().connect(
+ sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet));
+
+ sigc::connection connIconChanged;
+ connIconChanged = icon_view->signal_selection_changed().connect(
+ sigc::mem_fun(*this, &SymbolsDialog::iconChanged));
+ instanceConns.push_back(connIconChanged);
+
+ scroller = new Gtk::ScrolledWindow();
+ scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
+ scroller->add(*Gtk::manage(icon_view));
+ scroller->set_hexpand();
+ scroller->set_vexpand();
+
+ overlay = new Gtk::Overlay();
+ overlay->set_hexpand();
+ overlay->set_vexpand();
+ overlay->add(* scroller);
+ overlay->get_style_context()->add_class("forcebright");
+ scroller->set_size_request(100, 250);
+ table->attach(*Gtk::manage(overlay), 0, row, 2, 1);
+
+ /*************************Overlays******************************/
+ overlay_opacity = new Gtk::Image();
+ overlay_opacity->set_halign(Gtk::ALIGN_START);
+ overlay_opacity->set_valign(Gtk::ALIGN_START);
+ overlay_opacity->get_style_context()->add_class("rawstyle");
+
+ // No results
+ overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG);
+ overlay_icon->set_pixel_size(110);
+ overlay_icon->set_halign(Gtk::ALIGN_CENTER);
+ overlay_icon->set_valign(Gtk::ALIGN_START);
+
+ overlay_icon->set_margin_top(45);
+
+ overlay_title = new Gtk::Label();
+ overlay_title->set_halign(Gtk::ALIGN_CENTER );
+ overlay_title->set_valign(Gtk::ALIGN_START );
+ overlay_title->set_justify(Gtk::JUSTIFY_CENTER);
+ overlay_title->set_margin_top(155);
+
+ overlay_desc = new Gtk::Label();
+ overlay_desc->set_halign(Gtk::ALIGN_CENTER);
+ overlay_desc->set_valign(Gtk::ALIGN_START);
+ overlay_desc->set_margin_top(180);
+ overlay_desc->set_justify(Gtk::JUSTIFY_CENTER);
+
+ overlay->add_overlay(*overlay_opacity);
+ overlay->add_overlay(*overlay_icon);
+ overlay->add_overlay(*overlay_title);
+ overlay->add_overlay(*overlay_desc);
+
+ previous_height = 0;
+ previous_width = 0;
+ ++row;
+
+ /******************** Progress *******************************/
+ progress = new Gtk::HBox();
+ progress_bar = Gtk::manage(new Gtk::ProgressBar());
+ table->attach(*Gtk::manage(progress),0,row, 2, 1);
+ progress->pack_start(* progress_bar, Gtk::PACK_EXPAND_WIDGET);
+ progress->set_margin_top(15);
+ progress->set_margin_bottom(15);
+ progress->set_margin_start(20);
+ progress->set_margin_end(20);
+
+ ++row;
+
+ /******************** Tools *******************************/
+ tools = new Gtk::HBox();
+
+ //tools->set_layout( Gtk::BUTTONBOX_END );
+ scroller->set_hexpand();
+ table->attach(*Gtk::manage(tools),0,row,2,1);
+
+ auto add_symbol_image = Gtk::manage(sp_get_icon_image("symbol-add", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ add_symbol = Gtk::manage(new Gtk::Button());
+ add_symbol->add(*add_symbol_image);
+ add_symbol->set_tooltip_text(_("Add Symbol from the current document."));
+ add_symbol->set_relief( Gtk::RELIEF_NONE );
+ add_symbol->set_focus_on_click( false );
+ add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol));
+ tools->pack_start(* add_symbol, Gtk::PACK_SHRINK);
+
+ auto remove_symbolImage = Gtk::manage(sp_get_icon_image("symbol-remove", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ remove_symbol = Gtk::manage(new Gtk::Button());
+ remove_symbol->add(*remove_symbolImage);
+ remove_symbol->set_tooltip_text(_("Remove Symbol from the current document."));
+ remove_symbol->set_relief( Gtk::RELIEF_NONE );
+ remove_symbol->set_focus_on_click( false );
+ remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol));
+ tools->pack_start(* remove_symbol, Gtk::PACK_SHRINK);
+
+ Gtk::Label* spacer = Gtk::manage(new Gtk::Label(""));
+ tools->pack_start(* Gtk::manage(spacer));
+
+ // Pack size (controls display area)
+ pack_size = 2; // Default 32px
+
+ auto packMoreImage = Gtk::manage(sp_get_icon_image("pack-more", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ more = Gtk::manage(new Gtk::Button());
+ more->add(*packMoreImage);
+ more->set_tooltip_text(_("Display more icons in row."));
+ more->set_relief( Gtk::RELIEF_NONE );
+ more->set_focus_on_click( false );
+ more->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packmore));
+ tools->pack_start(* more, Gtk::PACK_SHRINK);
+
+ auto packLessImage = Gtk::manage(sp_get_icon_image("pack-less", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ fewer = Gtk::manage(new Gtk::Button());
+ fewer->add(*packLessImage);
+ fewer->set_tooltip_text(_("Display fewer icons in row."));
+ fewer->set_relief( Gtk::RELIEF_NONE );
+ fewer->set_focus_on_click( false );
+ fewer->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packless));
+ tools->pack_start(* fewer, Gtk::PACK_SHRINK);
+
+ // Toggle scale to fit on/off
+ auto fit_symbolImage = Gtk::manage(sp_get_icon_image("symbol-fit", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ fit_symbol = Gtk::manage(new Gtk::ToggleButton());
+ fit_symbol->add(*fit_symbolImage);
+ fit_symbol->set_tooltip_text(_("Toggle 'fit' symbols in icon space."));
+ fit_symbol->set_relief( Gtk::RELIEF_NONE );
+ fit_symbol->set_focus_on_click( false );
+ fit_symbol->set_active( true );
+ fit_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild));
+ tools->pack_start(* fit_symbol, Gtk::PACK_SHRINK);
+
+ // Render size (scales symbols within display area)
+ scale_factor = 0; // Default 1:1 * pack_size/pack_size default
+ auto zoom_outImage = Gtk::manage(sp_get_icon_image("symbol-smaller", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ zoom_out = Gtk::manage(new Gtk::Button());
+ zoom_out->add(*zoom_outImage);
+ zoom_out->set_tooltip_text(_("Make symbols smaller by zooming out."));
+ zoom_out->set_relief( Gtk::RELIEF_NONE );
+ zoom_out->set_focus_on_click( false );
+ zoom_out->set_sensitive( false );
+ zoom_out->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomout));
+ tools->pack_start(* zoom_out, Gtk::PACK_SHRINK);
+
+ auto zoom_inImage = Gtk::manage(sp_get_icon_image("symbol-bigger", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ zoom_in = Gtk::manage(new Gtk::Button());
+ zoom_in->add(*zoom_inImage);
+ zoom_in->set_tooltip_text(_("Make symbols bigger by zooming in."));
+ zoom_in->set_relief( Gtk::RELIEF_NONE );
+ zoom_in->set_focus_on_click( false );
+ zoom_in->set_sensitive( false );
+ zoom_in->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomin));
+ tools->pack_start(* zoom_in, Gtk::PACK_SHRINK);
+
+ ++row;
+
+ sensitive = true;
+
+ current_desktop = SP_ACTIVE_DESKTOP;
+ current_document = current_desktop->getDocument();
+ preview_document = symbolsPreviewDoc(); /* Template to render symbols in */
+ preview_document->ensureUpToDate(); /* Necessary? */
+ key = SPItem::display_key_new(1);
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY ));
+
+ // This might need to be a global variable so setTargetDesktop can modify it
+ SPDefs *defs = current_document->getDefs();
+ sigc::connection defsModifiedConn = defs->connectModified(sigc::mem_fun(*this, &SymbolsDialog::defsModified));
+ instanceConns.push_back(defsModifiedConn);
+
+ sigc::connection selectionChangedConn = current_desktop->selection->connectChanged(
+ sigc::mem_fun(*this, &SymbolsDialog::selectionChanged));
+ instanceConns.push_back(selectionChangedConn);
+
+ sigc::connection documentReplacedConn = current_desktop->connectDocumentReplaced(
+ sigc::mem_fun(*this, &SymbolsDialog::documentReplaced));
+ instanceConns.push_back(documentReplacedConn);
+ getSymbolsTitle();
+ icons_found = false;
+
+ addSymbolsInDoc(current_document); /* Defaults to current document */
+ sigc::connection desktopChangeConn =
+ desk_track.connectDesktopChanged( sigc::mem_fun(*this, &SymbolsDialog::setTargetDesktop) );
+ instanceConns.push_back( desktopChangeConn );
+ desk_track.connect(GTK_WIDGET(gobj()));
+}
+
+SymbolsDialog::~SymbolsDialog()
+{
+ for (auto & instanceConn : instanceConns) {
+ instanceConn.disconnect();
+ }
+ idleconn.disconnect();
+ instanceConns.clear();
+ desk_track.disconnect();
+}
+
+SymbolsDialog& SymbolsDialog::getInstance()
+{
+ return *new SymbolsDialog();
+}
+
+void SymbolsDialog::packless() {
+ if(pack_size < 4) {
+ pack_size++;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::packmore() {
+ if(pack_size > 0) {
+ pack_size--;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::zoomin() {
+ if(scale_factor < 4) {
+ scale_factor++;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::zoomout() {
+ if(scale_factor > -8) {
+ scale_factor--;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::rebuild() {
+
+ if (!sensitive) {
+ return;
+ }
+
+ if( fit_symbol->get_active() ) {
+ zoom_in->set_sensitive( false );
+ zoom_out->set_sensitive( false );
+ } else {
+ zoom_in->set_sensitive( true);
+ zoom_out->set_sensitive( true );
+ }
+ store->clear();
+ SPDocument* symbol_document = selectedSymbols();
+ icons_found = false;
+ //We are not in search all docs
+ if (search->get_text() != _("Searching...") && search->get_text() != _("Loading all symbols...")) {
+ Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text());
+ if (current == ALLDOCS && search->get_text() != "") {
+ searchsymbols();
+ return;
+ }
+ }
+ if (symbol_document) {
+ addSymbolsInDoc(symbol_document);
+ } else {
+ showOverlay();
+ }
+}
+void SymbolsDialog::showOverlay() {
+ Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text());
+ if (current == ALLDOCS && !l.size())
+ {
+ overlay_icon->hide();
+ if (!all_docs_processed ) {
+ overlay_icon->show();
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") +
+ Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("First search can be slow.")) + Glib::ustring("</span>"));
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term.")) + Glib::ustring("</span>"));
+ } else {
+ overlay_icon->show();
+ overlay_title->set_markup(Glib::ustring("<spansize=\"large\">") +
+ Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + Glib::ustring("</span>"));
+ }
+ } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) +
+ Glib::ustring("</span>"));
+ } else if (!number_symbols && current == CURRENTDOC) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")) +
+ Glib::ustring("</span>"));
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) +
+ Glib::ustring("</span>"));
+ }
+ gint width = scroller->get_allocated_width();
+ gint height = scroller->get_allocated_height();
+ if (previous_height != height || previous_width != width) {
+ previous_height = height;
+ previous_width = width;
+ overlay_opacity->set_size_request(width, height);
+ overlay_opacity->set(getOverlay(width, height));
+ }
+ overlay_opacity->hide();
+ overlay_icon->show();
+ overlay_title->show();
+ overlay_desc->show();
+ if (l.size()) {
+ overlay_opacity->show();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+ }
+}
+
+void SymbolsDialog::hideOverlay() {
+ overlay_opacity->hide();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+}
+
+void SymbolsDialog::insertSymbol() {
+ Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_SYMBOL );
+ SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop) );
+ sp_action_perform (action, nullptr);
+}
+
+void SymbolsDialog::revertSymbol() {
+ Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_UNSYMBOL );
+ SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop ) );
+ sp_action_perform (action, nullptr);
+}
+
+void SymbolsDialog::iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/)
+{
+ auto iconArray = icon_view->get_selected_items();
+
+ if( iconArray.empty() ) {
+ //std::cout << " iconArray empty: huh? " << std::endl;
+ } else {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ Glib::ustring symbol_id = (*row)[getColumns()->symbol_id];
+ GdkAtom dataAtom = gdk_atom_intern( "application/x-inkscape-paste", FALSE );
+ gtk_selection_data_set( data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length() );
+ }
+
+}
+
+void SymbolsDialog::defsModified(SPObject * /*object*/, guint /*flags*/)
+{
+ Glib::ustring doc_title = symbol_set->get_active_text();
+ if (doc_title != ALLDOCS && !symbol_sets[doc_title] ) {
+ rebuild();
+ }
+}
+
+void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) {
+ Glib::ustring symbol_id = selectedSymbolId();
+ Glib::ustring doc_title = selectedSymbolDocTitle();
+ if (!doc_title.empty()) {
+ SPDocument* symbol_document = symbol_sets[doc_title];
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ symbol_document = selectedSymbols();
+ }
+ if (symbol_document) {
+ SPObject* symbol = symbol_document->getObjectById(symbol_id);
+ if(symbol && !selection->includes(symbol)) {
+ icon_view->unselect_all();
+ }
+ }
+ }
+}
+
+void SymbolsDialog::documentReplaced(SPDesktop *desktop, SPDocument *document)
+{
+ current_desktop = desktop;
+ current_document = document;
+ rebuild();
+}
+
+SPDocument* SymbolsDialog::selectedSymbols() {
+ /* OK, we know symbol name... now we need to copy it to clipboard, bon chance! */
+ Glib::ustring doc_title = symbol_set->get_active_text();
+ if (doc_title == ALLDOCS) {
+ return nullptr;
+ }
+ SPDocument* symbol_document = symbol_sets[doc_title];
+ if( !symbol_document ) {
+ symbol_document = getSymbolsSet(doc_title).second;
+ // Symbol must be from Current Document (this method of checking should be language independent).
+ if( !symbol_document ) {
+ // Symbol must be from Current Document (this method of
+ // checking should be language independent).
+ symbol_document = current_document;
+ add_symbol->set_sensitive( true );
+ remove_symbol->set_sensitive( true );
+ } else {
+ add_symbol->set_sensitive( false );
+ remove_symbol->set_sensitive( false );
+ }
+ }
+ return symbol_document;
+}
+
+Glib::ustring SymbolsDialog::selectedSymbolId() {
+
+ auto iconArray = icon_view->get_selected_items();
+
+ if( !iconArray.empty() ) {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ return (*row)[getColumns()->symbol_id];
+ }
+ return Glib::ustring("");
+}
+
+Glib::ustring SymbolsDialog::selectedSymbolDocTitle() {
+
+ auto iconArray = icon_view->get_selected_items();
+
+ if( !iconArray.empty() ) {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ return (*row)[getColumns()->symbol_doc_title];
+ }
+ return Glib::ustring("");
+}
+
+Glib::ustring SymbolsDialog::documentTitle(SPDocument* symbol_doc) {
+ if (symbol_doc) {
+ SPRoot * root = symbol_doc->getRoot();
+ gchar * title = root->title();
+ if (title) {
+ return ellipsize(Glib::ustring(title), 33);
+ }
+ g_free(title);
+ }
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == CURRENTDOC) {
+ return current;
+ }
+ return _("Untitled document");
+}
+
+void SymbolsDialog::iconChanged() {
+
+ Glib::ustring symbol_id = selectedSymbolId();
+ SPDocument* symbol_document = selectedSymbols();
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ Glib::ustring doc_title = selectedSymbolDocTitle();
+ if (!doc_title.empty()) {
+ symbol_document = symbol_sets[doc_title];
+ }
+ }
+ if (symbol_document) {
+ SPObject* symbol = symbol_document->getObjectById(symbol_id);
+
+ if( symbol ) {
+ if( symbol_document == current_document ) {
+ // Select the symbol on the canvas so it can be manipulated
+ current_desktop->selection->set( symbol, false );
+ }
+ // Find style for use in <use>
+ // First look for default style stored in <symbol>
+ gchar const* style = symbol->getAttribute("inkscape:symbol-style");
+ if( !style ) {
+ // If no default style in <symbol>, look in documents.
+ if( symbol_document == current_document ) {
+ style = styleFromUse( symbol_id.c_str(), current_document );
+ } else {
+ style = symbol_document->getReprRoot()->attribute("style");
+ }
+ }
+
+ ClipboardManager *cm = ClipboardManager::get();
+ cm->copySymbol(symbol->getRepr(), style, symbol_document == current_document);
+ }
+ }
+}
+
+#ifdef WITH_LIBVISIO
+
+#if WITH_LIBVISIO01
+// Extend libvisio's native RVNGSVGDrawingGenerator with support for extracting stencil names (to be used as ID/title)
+class REVENGE_API RVNGSVGDrawingGenerator_WithTitle : public RVNGSVGDrawingGenerator {
+ public:
+ RVNGSVGDrawingGenerator_WithTitle(RVNGStringVector &output, RVNGStringVector &titles, const RVNGString &nmSpace)
+ : RVNGSVGDrawingGenerator(output, nmSpace)
+ , _titles(titles)
+ {}
+
+ void startPage(const RVNGPropertyList &propList) override
+ {
+ RVNGSVGDrawingGenerator::startPage(propList);
+ if (propList["draw:name"]) {
+ _titles.append(propList["draw:name"]->getStr());
+ } else {
+ _titles.append("");
+ }
+ }
+
+ private:
+ RVNGStringVector &_titles;
+};
+#endif
+
+// Read Visio stencil files
+SPDocument* read_vss(Glib::ustring filename, Glib::ustring name ) {
+ gchar *fullname;
+ #ifdef _WIN32
+ // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows
+ // therefore attempt to convert uri to the system codepage
+ // even if this is not possible the alternate short (8.3) file name will be used if available
+ fullname = g_win32_locale_filename_from_utf8(filename.c_str());
+ #else
+ fullname = strdup(filename.c_str());
+ #endif
+
+ RVNGFileStream input(fullname);
+ g_free(fullname);
+
+ if (!libvisio::VisioDocument::isSupported(&input)) {
+ return nullptr;
+ }
+ RVNGStringVector output;
+ RVNGStringVector titles;
+#if WITH_LIBVISIO01
+ RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg");
+
+ if (!libvisio::VisioDocument::parseStencils(&input, &generator)) {
+#else
+ if (!libvisio::VisioDocument::generateSVGStencils(&input, output)) {
+#endif
+ return nullptr;
+ }
+ if (output.empty()) {
+ return nullptr;
+ }
+
+ // prepare a valid title for the symbol file
+ Glib::ustring title = Glib::Markup::escape_text(name);
+ // prepare a valid id prefix for symbols libvisio doesn't give us a name for
+ Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("[^a-zA-Z0-9_-]");
+ Glib::ustring id = regex1->replace(name, 0, "_", Glib::REGEX_MATCH_PARTIAL);
+
+ Glib::ustring tmpSVGOutput;
+ tmpSVGOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
+ tmpSVGOutput += "<svg\n";
+ tmpSVGOutput += " xmlns=\"http://www.w3.org/2000/svg\"\n";
+ tmpSVGOutput += " xmlns:svg=\"http://www.w3.org/2000/svg\"\n";
+ tmpSVGOutput += " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n";
+ tmpSVGOutput += " version=\"1.1\"\n";
+ tmpSVGOutput += " style=\"fill:none;stroke:#000000;stroke-width:2\">\n";
+ tmpSVGOutput += " <title>";
+ tmpSVGOutput += title;
+ tmpSVGOutput += "</title>\n";
+ tmpSVGOutput += " <defs>\n";
+
+ // Each "symbol" is in its own SVG file, we wrap with <symbol> and merge into one file.
+ for (unsigned i=0; i<output.size(); ++i) {
+
+ std::stringstream ss;
+ if (titles.size() == output.size() && titles[i] != "") {
+ // TODO: Do we need to check for duplicated titles?
+ ss << regex1->replace(titles[i].cstr(), 0, "_", Glib::REGEX_MATCH_PARTIAL);
+ } else {
+ ss << id << "_" << i;
+ }
+
+ tmpSVGOutput += " <symbol id=\"" + ss.str() + "\">\n";
+
+#if WITH_LIBVISIO01
+ if (titles.size() == output.size() && titles[i] != "") {
+ tmpSVGOutput += " <title>" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "</title>\n";
+ }
+#endif
+
+ std::istringstream iss( output[i].cstr() );
+ std::string line;
+ while( std::getline( iss, line ) ) {
+ if( line.find( "svg:svg" ) == std::string::npos ) {
+ tmpSVGOutput += " " + line + "\n";
+ }
+ }
+
+ tmpSVGOutput += " </symbol>\n";
+ }
+
+ tmpSVGOutput += " </defs>\n";
+ tmpSVGOutput += "</svg>\n";
+ return SPDocument::createNewDocFromMem( tmpSVGOutput.c_str(), strlen( tmpSVGOutput.c_str()), false );
+
+}
+#endif
+
+/* Hunts preference directories for symbol files */
+void SymbolsDialog::getSymbolsTitle() {
+
+ using namespace Inkscape::IO::Resource;
+ Glib::ustring title;
+ number_docs = 0;
+ std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)");
+ for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) {
+ if(Glib::str_has_suffix(filename, ".vss")) {
+ std::size_t found = filename.find_last_of("/\\");
+ filename = filename.substr(found+1);
+ title = filename.erase(filename.rfind('.'));
+ if(title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[title]= nullptr;
+ ++number_docs;
+ } else {
+ std::ifstream infile(filename);
+ std::string line;
+ while (std::getline(infile, line)) {
+ std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy);
+ if (!title_res.empty()) {
+ title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str());
+ symbol_sets[ellipsize(Glib::ustring(title_res), 33)]= nullptr;
+ ++number_docs;
+ break;
+ }
+ std::string::size_type position_exit = line.find ("<defs");
+ if (position_exit != std::string::npos) {
+ std::size_t found = filename.find_last_of("/\\");
+ filename = filename.substr(found+1);
+ title = filename.erase(filename.rfind('.'));
+ if(title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[title]= nullptr;
+ ++number_docs;
+ break;
+ }
+ }
+ }
+ }
+ for(auto const &symbol_document_map : symbol_sets) {
+ symbol_set->append(symbol_document_map.first);
+ }
+}
+
+/* Hunts preference directories for symbol files */
+std::pair<Glib::ustring, SPDocument*>
+SymbolsDialog::getSymbolsSet(Glib::ustring title)
+{
+ SPDocument* symbol_doc = nullptr;
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == CURRENTDOC) {
+ return std::make_pair(CURRENTDOC, symbol_doc);
+ }
+ if (symbol_sets[title]) {
+ sensitive = false;
+ symbol_set->remove_all();
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ for(auto const &symbol_document_map : symbol_sets) {
+ if (CURRENTDOC != symbol_document_map.first) {
+ symbol_set->append(symbol_document_map.first);
+ }
+ }
+ symbol_set->set_active_text(title);
+ sensitive = true;
+ return std::make_pair(title, symbol_sets[title]);
+ }
+ using namespace Inkscape::IO::Resource;
+ Glib::ustring new_title;
+
+ std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)");
+ for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) {
+ if(Glib::str_has_suffix(filename, ".vss")) {
+#ifdef WITH_LIBVISIO
+ std::size_t pos = filename.find_last_of("/\\");
+ Glib::ustring filename_short = "";
+ if (pos != std::string::npos) {
+ filename_short = filename.substr(pos+1);
+ }
+ if (filename_short == title + ".vss") {
+ new_title = title;
+ symbol_doc = read_vss(Glib::ustring(filename), title);
+ }
+#endif
+ } else {
+ std::ifstream infile(filename);
+ std::string line;
+ while (std::getline(infile, line)) {
+ std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy);
+ if (!title_res.empty()) {
+ title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str());
+ new_title = ellipsize(Glib::ustring(title_res), 33);
+ }
+ std::size_t pos = filename.find_last_of("/\\");
+ Glib::ustring filename_short = "";
+ if (pos != std::string::npos) {
+ filename_short = filename.substr(pos+1);
+ }
+ if (title == new_title || filename_short == title + ".svg") {
+ new_title = title;
+ if(Glib::str_has_suffix(filename, ".svg")) {
+ symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE);
+ }
+ }
+ if (symbol_doc) {
+ break;
+ }
+ std::string::size_type position_exit = line.find ("<defs");
+ if (position_exit != std::string::npos) {
+ break;
+ }
+ }
+ }
+ if (symbol_doc) {
+ break;
+ }
+ }
+ if(symbol_doc) {
+ symbol_sets.erase(title);
+ symbol_sets[new_title] = symbol_doc;
+ sensitive = false;
+ symbol_set->remove_all();
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ for(auto const &symbol_document_map : symbol_sets) {
+ if (CURRENTDOC != symbol_document_map.first) {
+ symbol_set->append(symbol_document_map.first);
+ }
+ }
+ symbol_set->set_active_text(new_title);
+ sensitive = true;
+ }
+ return std::make_pair(new_title, symbol_doc);
+}
+
+void SymbolsDialog::symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title)
+{
+ if(!r) return;
+
+ // Stop multiple counting of same symbol
+ if ( dynamic_cast<SPUse *>(r) ) {
+ return;
+ }
+
+ if ( dynamic_cast<SPSymbol *>(r)) {
+ Glib::ustring id = r->getAttribute("id");
+ gchar * title = r->title();
+ if(title) {
+ l[doc_title + title + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r));
+ } else {
+ l[Glib::ustring(_("notitle_")) + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r));
+ }
+ g_free(title);
+ }
+ for (auto& child: r->children) {
+ symbolsInDocRecursive(&child, l, doc_title);
+ }
+}
+
+std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> >
+SymbolsDialog::symbolsInDoc( SPDocument* symbol_document, Glib::ustring doc_title)
+{
+
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l;
+ if (symbol_document) {
+ symbolsInDocRecursive (symbol_document->getRoot(), l , doc_title);
+ }
+ return l;
+}
+
+void SymbolsDialog::useInDoc (SPObject *r, std::vector<SPUse*> &l)
+{
+
+ if ( dynamic_cast<SPUse *>(r) ) {
+ l.push_back(dynamic_cast<SPUse *>(r));
+ }
+
+ for (auto& child: r->children) {
+ useInDoc( &child, l );
+ }
+}
+
+std::vector<SPUse*> SymbolsDialog::useInDoc( SPDocument* useDocument) {
+ std::vector<SPUse*> l;
+ useInDoc (useDocument->getRoot(), l);
+ return l;
+}
+
+// Returns style from first <use> element found that references id.
+// This is a last ditch effort to find a style.
+gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) {
+
+ gchar const* style = nullptr;
+ std::vector<SPUse*> l = useInDoc( document );
+ for( auto use:l ) {
+ if ( use ) {
+ gchar const *href = use->getRepr()->attribute("xlink:href");
+ if( href ) {
+ Glib::ustring href2(href);
+ Glib::ustring id2(id);
+ id2 = "#" + id2;
+ if( !href2.compare(id2) ) {
+ style = use->getRepr()->attribute("style");
+ break;
+ }
+ }
+ }
+ }
+ return style;
+}
+
+void SymbolsDialog::clearSearch()
+{
+ if(search->get_text().empty() && sensitive) {
+ enableWidgets(false);
+ search_str = "";
+ store->clear();
+ SPDocument* symbol_document = selectedSymbols();
+ if (symbol_document) {
+ //We are not in search all docs
+ icons_found = false;
+ addSymbolsInDoc(symbol_document);
+ } else {
+ showOverlay();
+ enableWidgets(true);
+ }
+ }
+}
+
+void SymbolsDialog::enableWidgets(bool enable)
+{
+ symbol_set->set_sensitive(enable);
+ search->set_sensitive(enable);
+ tools ->set_sensitive(enable);
+}
+
+void SymbolsDialog::beforeSearch(GdkEventKey* evt)
+{
+ sensitive = false;
+ search_str = search->get_text().lowercase();
+ if (evt->keyval != GDK_KEY_Return) {
+ return;
+ }
+ searchsymbols();
+}
+
+void SymbolsDialog::searchsymbols()
+{
+ progress_bar->set_fraction(0.0);
+ enableWidgets(false);
+ SPDocument *symbol_document = selectedSymbols();
+ if (symbol_document) {
+ // We are not in search all docs
+ search->set_text(_("Searching..."));
+ store->clear();
+ icons_found = false;
+ addSymbolsInDoc(symbol_document);
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect(sigc::mem_fun(*this, &SymbolsDialog::callbackAllSymbols));
+ search->set_text(_("Loading all symbols..."));
+ }
+}
+
+void SymbolsDialog::unsensitive(GdkEventKey* evt)
+{
+ sensitive = true;
+}
+
+bool SymbolsDialog::callbackSymbols(){
+ if (l.size()) {
+ showOverlay();
+ for (auto symbol_data = l.begin(); symbol_data != l.end();) {
+ Glib::ustring doc_title = symbol_data->second.first;
+ SPSymbol * symbol = symbol_data->second.second;
+ counter_symbols ++;
+ gchar *symbol_title_char = symbol->title();
+ gchar *symbol_desc_char = symbol->description();
+ bool found = false;
+ if (symbol_title_char) {
+ Glib::ustring symbol_title = Glib::ustring(symbol_title_char).lowercase();
+ auto pos = symbol_title.rfind(search_str);
+ if (pos != std::string::npos) {
+ found = true;
+ }
+ if (!found && symbol_desc_char) {
+ Glib::ustring symbol_desc = Glib::ustring(symbol_desc_char).lowercase();
+ auto pos = symbol_desc.rfind(search_str);
+ if (pos != std::string::npos) {
+ found = true;
+ }
+ }
+ }
+ if (symbol && (search_str.empty() || found)) {
+ addSymbol( symbol, doc_title);
+ icons_found = true;
+ }
+
+ progress_bar->set_fraction(((100.0/number_symbols) * counter_symbols)/100.0);
+ symbol_data = l.erase(l.begin());
+ //to get more items and best performance
+ int modulus = number_symbols > 200 ? 50 : (number_symbols/4);
+ g_free(symbol_title_char);
+ g_free(symbol_desc_char);
+ if (modulus && counter_symbols % modulus == 0 && !l.empty()) {
+ return true;
+ }
+ }
+ if (!icons_found && !search_str.empty()) {
+ showOverlay();
+ } else {
+ hideOverlay();
+ }
+ progress_bar->set_fraction(0);
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ return false;
+ }
+ return true;
+}
+
+bool SymbolsDialog::callbackAllSymbols(){
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == ALLDOCS && search->get_text() == _("Loading all symbols...")) {
+ size_t counter = 0;
+ std::map<Glib::ustring, SPDocument*> symbol_sets_tmp = symbol_sets;
+ for(auto const &symbol_document_map : symbol_sets_tmp) {
+ ++counter;
+ SPDocument* symbol_document = symbol_document_map.second;
+ if (symbol_document) {
+ continue;
+ }
+ symbol_document = getSymbolsSet(symbol_document_map.first).second;
+ symbol_set->set_active_text(ALLDOCS);
+ if (!symbol_document) {
+ continue;
+ }
+ progress_bar->set_fraction(((100.0/number_docs) * counter)/100.0);
+ return true;
+ }
+ symbol_sets_tmp.clear();
+ hideOverlay();
+ all_docs_processed = true;
+ addSymbols();
+ progress_bar->set_fraction(0);
+ search->set_text("Searching...");
+ return false;
+ }
+ return true;
+}
+
+Glib::ustring SymbolsDialog::ellipsize(Glib::ustring data, size_t limit) {
+ if (data.length() > limit) {
+ data = data.substr(0, limit-3);
+ return data + "...";
+ }
+ return data;
+}
+
+void SymbolsDialog::addSymbolsInDoc(SPDocument* symbol_document) {
+
+ if (!symbol_document) {
+ return; //Search all
+ }
+ Glib::ustring doc_title = documentTitle(symbol_document);
+ progress_bar->set_fraction(0.0);
+ counter_symbols = 0;
+ l = symbolsInDoc(symbol_document, doc_title);
+ number_symbols = l.size();
+ if (!number_symbols) {
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ idleconn.disconnect();
+ showOverlay();
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols));
+ }
+}
+
+void SymbolsDialog::addSymbols() {
+ store->clear();
+ icons_found = false;
+ for(auto const &symbol_document_map : symbol_sets) {
+ SPDocument* symbol_document = symbol_document_map.second;
+ if (!symbol_document) {
+ continue;
+ }
+ Glib::ustring doc_title = documentTitle(symbol_document);
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l_tmp = symbolsInDoc(symbol_document, doc_title);
+ for(auto &p : l_tmp ) {
+ l[p.first] = p.second;
+ }
+ l_tmp.clear();
+ }
+ counter_symbols = 0;
+ progress_bar->set_fraction(0.0);
+ number_symbols = l.size();
+ if (!number_symbols) {
+ showOverlay();
+ idleconn.disconnect();
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols));
+ }
+}
+
+void SymbolsDialog::addSymbol( SPObject* symbol, Glib::ustring doc_title)
+{
+ gchar const *id = symbol->getRepr()->attribute("id");
+
+ if (doc_title.empty()) {
+ doc_title = CURRENTDOC;
+ } else {
+ doc_title = g_dpgettext2(nullptr, "Symbol", doc_title.c_str());
+ }
+
+ Glib::ustring symbol_title;
+ gchar *title = symbol->title(); // From title element
+ if (title) {
+ symbol_title = Glib::ustring::compose("%1 (%2)", g_dpgettext2(nullptr, "Symbol", title), doc_title.c_str());
+ } else {
+ symbol_title = Glib::ustring::compose("%1 %2 (%3)", _("Symbol without title"), Glib::ustring(id), doc_title);
+ }
+ g_free(title);
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = drawSymbol( symbol );
+ if( pixbuf ) {
+ Gtk::ListStore::iterator row = store->append();
+ SymbolColumns* columns = getColumns();
+ (*row)[columns->symbol_id] = Glib::ustring( id );
+ (*row)[columns->symbol_title] = Glib::Markup::escape_text(symbol_title);
+ (*row)[columns->symbol_doc_title] = Glib::Markup::escape_text(doc_title);
+ (*row)[columns->symbol_image] = pixbuf;
+ delete columns;
+ }
+}
+
+/*
+ * Returns image of symbol.
+ *
+ * Symbols normally are not visible. They must be referenced by a
+ * <use> element. A temporary document is created with a dummy
+ * <symbol> element and a <use> element that references the symbol
+ * element. Each real symbol is swapped in for the dummy symbol and
+ * the temporary document is rendered.
+ */
+Glib::RefPtr<Gdk::Pixbuf>
+SymbolsDialog::drawSymbol(SPObject *symbol)
+{
+ // Create a copy repr of the symbol with id="the_symbol"
+ Inkscape::XML::Document *xml_doc = preview_document->getReprDoc();
+ Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(xml_doc);
+ repr->setAttribute("id", "the_symbol");
+
+ // Replace old "the_symbol" in preview_document by new.
+ Inkscape::XML::Node *root = preview_document->getReprRoot();
+ SPObject *symbol_old = preview_document->getObjectById("the_symbol");
+ if (symbol_old) {
+ symbol_old->deleteObject(false);
+ }
+
+ // First look for default style stored in <symbol>
+ gchar const* style = repr->attribute("inkscape:symbol-style");
+ if( !style ) {
+ // If no default style in <symbol>, look in documents.
+ if( symbol->document == current_document ) {
+ gchar const *id = symbol->getRepr()->attribute("id");
+ style = styleFromUse( id, symbol->document );
+ } else {
+ style = symbol->document->getReprRoot()->attribute("style");
+ }
+ }
+ // Last ditch effort to provide some default styling
+ if( !style ) style = "fill:#bbbbbb;stroke:#808080";
+
+ // This is for display in Symbols dialog only
+ if( style ) repr->setAttribute( "style", style );
+
+ // BUG: Symbols don't work if defined outside of <defs>. Causes Inkscape
+ // crash when trying to read in such a file.
+ root->appendChild(repr);
+ //defsrepr->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Uncomment this to get the preview_document documents saved (useful for debugging)
+ // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w");
+ // sp_repr_save_stream(preview_document->getReprDoc(), fp);
+ // fclose (fp);
+
+ // Make sure preview_document is up-to-date.
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ // Make sure we have symbol in preview_document
+ SPObject *object_temp = preview_document->getObjectById( "the_use" );
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ SPItem *item = dynamic_cast<SPItem *>(object_temp);
+ g_assert(item != nullptr);
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size];
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr);
+ // We could use cache here, but it doesn't really work with the structure
+ // of this user interface and we've already cached the pixbuf in the gtklist
+
+ // Find object's bbox in document.
+ // Note symbols can have own viewport... ignore for now.
+ //Geom::OptRect dbox = item->geometricBounds();
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (dbox) {
+ /* Scale symbols to fit */
+ double scale = 1.0;
+ double width = dbox->width();
+ double height = dbox->height();
+
+ if( width == 0.0 ) width = 1.0;
+ if( height == 0.0 ) height = 1.0;
+
+ if( fit_symbol->get_active() )
+ scale = psize / ceil(std::max(width, height));
+ else
+ scale = pow( 2.0, scale_factor/2.0 ) * psize / 32.0;
+
+ pixbuf = Glib::wrap(render_pixbuf(renderDrawing, scale, *dbox, psize));
+ }
+
+ return pixbuf;
+}
+
+/*
+ * Return empty doc to render symbols in.
+ * Symbols are by default not rendered so a <use> element is
+ * provided.
+ */
+SPDocument* SymbolsDialog::symbolsPreviewDoc()
+{
+ // BUG: <symbol> must be inside <defs>
+ gchar const *buffer =
+"<svg xmlns=\"http://www.w3.org/2000/svg\""
+" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\""
+" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\""
+" xmlns:xlink=\"http://www.w3.org/1999/xlink\">"
+" <defs id=\"defs\">"
+" <symbol id=\"the_symbol\"/>"
+" </defs>"
+" <use id=\"the_use\" xlink:href=\"#the_symbol\"/>"
+"</svg>";
+ return SPDocument::createNewDocFromMem( buffer, strlen(buffer), FALSE );
+}
+
+/*
+ * Update image widgets
+ */
+Glib::RefPtr<Gdk::Pixbuf>
+SymbolsDialog::getOverlay(gint width, gint height)
+{
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+ cr = cairo_create (surface);
+ cairo_set_source_rgba(cr, 1, 1, 1, 0.75);
+ cairo_rectangle (cr, 0, 0, width, height);
+ cairo_fill (cr);
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(surface);
+ cairo_destroy (cr);
+ return Glib::wrap(pixbuf);
+}
+
+void SymbolsDialog::setTargetDesktop(SPDesktop *desktop)
+{
+ if (this->current_desktop != desktop) {
+ this->current_desktop = desktop;
+ if( !symbol_sets[symbol_set->get_active_text()] ) {
+ // Symbol set is from Current document, update
+ rebuild();
+ }
+ }
+}
+
+} //namespace Dialogs
+} //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=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/symbols.h b/src/ui/dialog/symbols.h
new file mode 100644
index 0000000..382d310
--- /dev/null
+++ b/src/ui/dialog/symbols.h
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Symbols dialog
+ */
+/* Authors:
+ * Tavmjong Bah, Martin Owens
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * 2013 Martin Owens
+ * 2017 Jabiertxo Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_SYMBOLS_H
+#define INKSCAPE_UI_DIALOG_SYMBOLS_H
+
+#include <vector>
+
+#include <gtkmm.h>
+
+#include "display/drawing.h"
+#include "include/gtkmm_version.h"
+#include "ui/dialog/desktop-tracker.h"
+#include "ui/widget/panel.h"
+
+class SPObject;
+class SPSymbol;
+class SPUse;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class SymbolColumns; // For Gtk::ListStore
+
+/**
+ * A dialog that displays selectable symbols and allows users to drag or paste
+ * those symbols from the dialog into the document.
+ *
+ * Symbol documents are loaded from the preferences paths and displayed in a
+ * drop-down list to the user. The user then selects which of the symbols
+ * documents they want to get symbols from. The first document in the list is
+ * always the current document.
+ *
+ * This then updates an icon-view with all the symbols available. Selecting one
+ * puts it onto the clipboard. Dragging it or pasting it onto the canvas copies
+ * the symbol from the symbol document, into the current document and places a
+ * new <use element at the correct location on the canvas.
+ *
+ * Selected groups on the canvas can be added to the current document's symbols
+ * table, and symbols can be removed from the current document. This allows
+ * new symbols documents to be constructed and if saved in the prefs folder will
+ * make those symbols available for all future documents.
+ */
+
+const int SYMBOL_ICON_SIZES[] = {16, 24, 32, 48, 64};
+
+class SymbolsDialog : public UI::Widget::Panel {
+
+public:
+ SymbolsDialog( gchar const* prefsPath = "/dialogs/symbols" );
+ ~SymbolsDialog() override;
+
+ static SymbolsDialog& getInstance();
+
+private:
+ SymbolsDialog(SymbolsDialog const &) = delete; // no copy
+ SymbolsDialog &operator=(SymbolsDialog const &) = delete; // no assign
+
+ static SymbolColumns *getColumns();
+
+ Glib::ustring CURRENTDOC;
+ Glib::ustring ALLDOCS;
+
+ void packless();
+ void packmore();
+ void zoomin();
+ void zoomout();
+ void rebuild();
+ void insertSymbol();
+ void revertSymbol();
+ void defsModified(SPObject *object, guint flags);
+ void selectionChanged(Inkscape::Selection *selection);
+ void documentReplaced(SPDesktop *desktop, SPDocument *document);
+ SPDocument* selectedSymbols();
+ Glib::ustring selectedSymbolId();
+ Glib::ustring selectedSymbolDocTitle();
+ void iconChanged();
+ void iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time);
+ void getSymbolsTitle();
+ Glib::ustring documentTitle(SPDocument* doc);
+ std::pair<Glib::ustring, SPDocument*> getSymbolsSet(Glib::ustring title);
+ void addSymbol( SPObject* symbol, Glib::ustring doc_title);
+ SPDocument* symbolsPreviewDoc();
+ void symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title);
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > symbolsInDoc( SPDocument* document, Glib::ustring doc_title);
+ void useInDoc(SPObject *r, std::vector<SPUse*> &l);
+ std::vector<SPUse*> useInDoc( SPDocument* document);
+ void beforeSearch(GdkEventKey* evt);
+ void unsensitive(GdkEventKey* evt);
+ void searchsymbols();
+ void addSymbols();
+ void addSymbolsInDoc(SPDocument* document);
+ void showOverlay();
+ void hideOverlay();
+ void clearSearch();
+ bool callbackSymbols();
+ bool callbackAllSymbols();
+ void enableWidgets(bool enable);
+ Glib::ustring ellipsize(Glib::ustring data, size_t limit);
+ gchar const* styleFromUse( gchar const* id, SPDocument* document);
+ Glib::RefPtr<Gdk::Pixbuf> drawSymbol(SPObject *symbol);
+ Glib::RefPtr<Gdk::Pixbuf> getOverlay(gint width, gint height);
+ /* Keep track of all symbol template documents */
+ std::map<Glib::ustring, SPDocument*> symbol_sets;
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l;
+ // Index into sizes which is selected
+ int pack_size;
+ // Scale factor
+ int scale_factor;
+ bool sensitive;
+ double previous_height;
+ double previous_width;
+ bool all_docs_processed;
+ size_t number_docs;
+ size_t number_symbols;
+ size_t counter_symbols;
+ bool icons_found;
+ Glib::RefPtr<Gtk::ListStore> store;
+ Glib::ustring search_str;
+ Gtk::ComboBoxText* symbol_set;
+ Gtk::ProgressBar* progress_bar;
+ Gtk::HBox* progress;
+ Gtk::SearchEntry* search;
+ Gtk::IconView* icon_view;
+ Gtk::Button* add_symbol;
+ Gtk::Button* remove_symbol;
+ Gtk::Button* zoom_in;
+ Gtk::Button* zoom_out;
+ Gtk::Button* more;
+ Gtk::Button* fewer;
+ Gtk::HBox* tools;
+ Gtk::Overlay* overlay;
+ Gtk::Image* overlay_icon;
+ Gtk::Image* overlay_opacity;
+ Gtk::Label* overlay_title;
+ Gtk::Label* overlay_desc;
+ Gtk::ScrolledWindow *scroller;
+ Gtk::ToggleButton* fit_symbol;
+ Gtk::IconSize iconsize;
+ void setTargetDesktop(SPDesktop *desktop);
+ SPDesktop* current_desktop;
+ DesktopTracker desk_track;
+ SPDocument* current_document;
+ SPDocument* preview_document; /* Document to render single symbol */
+
+ sigc::connection idleconn;
+
+ /* For rendering the template drawing */
+ unsigned key;
+ Inkscape::Drawing renderDrawing;
+
+ std::vector<sigc::connection> instanceConns;
+};
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif // INKSCAPE_UI_DIALOG_SYMBOLS_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/tags.cpp b/src/ui/dialog/tags.cpp
new file mode 100644
index 0000000..35400ab
--- /dev/null
+++ b/src/ui/dialog/tags.cpp
@@ -0,0 +1,1125 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple panel for tags
+ *
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tags.h"
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <glibmm/main.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "layer-fns.h"
+#include "layer-manager.h"
+#include "verbs.h"
+
+#include "helper/action.h"
+#include "include/gtkmm_version.h"
+#include "object/sp-defs.h"
+#include "object/sp-item.h"
+#include "object/sp-object-group.h"
+#include "svg/css-ostringstream.h"
+#include "ui/icon-loader.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/iconrenderer.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;
+
+TagsPanel& TagsPanel::getInstance()
+{
+ return *new TagsPanel();
+}
+
+enum {
+ COL_ADD = 1
+};
+
+enum {
+ BUTTON_NEW = 0,
+ BUTTON_TOP,
+ BUTTON_BOTTOM,
+ BUTTON_UP,
+ BUTTON_DOWN,
+ BUTTON_DELETE,
+ DRAGNDROP
+};
+
+class TagsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver {
+public:
+ ObjectWatcher(TagsPanel* pnl, SPObject* obj, Inkscape::XML::Node * repr) :
+ _pnl(pnl),
+ _obj(obj),
+ _repr(repr),
+ _labelAttr(g_quark_from_string("inkscape:label"))
+ {}
+
+ ObjectWatcher(TagsPanel* pnl, SPObject* obj) :
+ _pnl(pnl),
+ _obj(obj),
+ _repr(obj->getRepr()),
+ _labelAttr(g_quark_from_string("inkscape:label"))
+ {}
+
+ void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override
+ {
+ if ( _pnl && _obj ) {
+ _pnl->_objectsChanged( _obj );
+ }
+ }
+ void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override
+ {
+ if ( _pnl && _obj ) {
+ _pnl->_objectsChanged( _obj );
+ }
+ }
+ void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override
+ {
+ if ( _pnl && _obj ) {
+ _pnl->_objectsChanged( _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 {
+
+ static GQuark const _labelID = g_quark_from_string("id");
+
+ if ( _pnl && _obj ) {
+ if ( name == _labelAttr || name == _labelID ) {
+ _pnl->_updateObject( _obj);
+ }
+ }
+ }
+
+ TagsPanel* _pnl;
+ SPObject* _obj;
+ Inkscape::XML::Node* _repr;
+ GQuark _labelAttr;
+};
+
+class TagsPanel::InternalUIBounce
+{
+public:
+ int _actionCode;
+};
+
+void TagsPanel::_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(*manage(Glib::wrap(child)));
+ btn.set_relief(Gtk::RELIEF_NONE);
+ btn.set_tooltip_text (tooltip);
+}
+
+
+Gtk::MenuItem& TagsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback, int id )
+{
+ GtkWidget* iconWidget = nullptr;
+ const char* label = nullptr;
+
+ if ( iconName ) {
+ iconWidget = sp_get_icon_image(iconName, GTK_ICON_SIZE_MENU);
+ }
+
+ if ( desktop ) {
+ Verb *verb = Verb::get( code );
+ if ( verb ) {
+ SPAction *action = verb->get_action(desktop);
+ if ( !iconWidget && action && action->image ) {
+ iconWidget = sp_get_icon_image(action->image, GTK_ICON_SIZE_MENU);
+ }
+
+ if ( action ) {
+ // label = action->name;
+ }
+ }
+ }
+
+ if ( !label && fallback ) {
+ label = fallback;
+ }
+
+ Gtk::Widget* wrapped = nullptr;
+ if ( iconWidget ) {
+ wrapped = Gtk::manage(Glib::wrap(iconWidget));
+ wrapped->show();
+ }
+
+
+ Gtk::MenuItem* item = nullptr;
+
+ if (wrapped) {
+ item = Gtk::manage(new Gtk::ImageMenuItem(*wrapped, label, true));
+ } else {
+ item = Gtk::manage(new Gtk::MenuItem(label, true));
+ }
+
+ item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &TagsPanel::_takeAction), id));
+ _popupMenu.append(*item);
+
+ return *item;
+}
+
+void TagsPanel::_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 );
+ }
+ }
+ }
+}
+
+void TagsPanel::_takeAction( int val )
+{
+ if ( !_pending ) {
+ _pending = new InternalUIBounce();
+ _pending->_actionCode = val;
+ Glib::signal_timeout().connect( sigc::mem_fun(*this, &TagsPanel::_executeAction), 0 );
+ }
+}
+
+bool TagsPanel::_executeAction()
+{
+ // Make sure selected layer hasn't changed since the action was triggered
+ if ( _pending)
+ {
+ int val = _pending->_actionCode;
+// SPObject* target = _pending->_target;
+ bool empty = _desktop->selection->isEmpty();
+
+ switch ( val ) {
+ case BUTTON_NEW:
+ {
+ _fireAction( SP_VERB_TAG_NEW );
+ }
+ break;
+ case BUTTON_TOP:
+ {
+ _fireAction( empty ? SP_VERB_LAYER_TO_TOP : SP_VERB_SELECTION_TO_FRONT);
+ }
+ break;
+ case BUTTON_BOTTOM:
+ {
+ _fireAction( empty ? SP_VERB_LAYER_TO_BOTTOM : SP_VERB_SELECTION_TO_BACK );
+ }
+ break;
+ case BUTTON_UP:
+ {
+ _fireAction( empty ? SP_VERB_LAYER_RAISE : SP_VERB_SELECTION_RAISE );
+ }
+ break;
+ case BUTTON_DOWN:
+ {
+ _fireAction( empty ? SP_VERB_LAYER_LOWER : SP_VERB_SELECTION_LOWER );
+ }
+ break;
+ case BUTTON_DELETE:
+ {
+ std::vector<SPObject *> todelete;
+ _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete));
+ for (auto obj : todelete) {
+ if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) {
+ //obj->parent->getRepr()->removeChild(obj->getRepr());
+ obj->deleteObject(true, true);
+ }
+ }
+ DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set"));
+ }
+ break;
+ case DRAGNDROP:
+ {
+ _doTreeMove( );
+ }
+ break;
+ }
+
+ delete _pending;
+ _pending = nullptr;
+ }
+
+ return false;
+}
+
+
+class TagsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+
+ ModelColumns()
+ {
+ add(_colParentObject);
+ add(_colObject);
+ add(_colLabel);
+ add(_colAddRemove);
+ add(_colAllowAddRemove);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<SPObject*> _colParentObject;
+ Gtk::TreeModelColumn<SPObject*> _colObject;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<int> _colAddRemove;
+ Gtk::TreeModelColumn<bool> _colAllowAddRemove;
+};
+
+void TagsPanel::_checkForDeleted(const Gtk::TreeIter& iter, std::vector<SPObject *>* todelete)
+{
+ Gtk::TreeRow row = *iter;
+ SPObject * obj = row[_model->_colObject];
+ if (obj && obj->parent) {
+ todelete->push_back(obj);
+ }
+}
+
+void TagsPanel::_updateObject( SPObject *obj ) {
+ _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &TagsPanel::_checkForUpdated), obj) );
+}
+
+bool TagsPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj)
+{
+ Gtk::TreeModel::Row row = *iter;
+ if ( obj == 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->getId();
+ gchar const *label;
+ SPTagUse * use = SP_IS_TAG_USE(obj) ? SP_TAG_USE(obj) : nullptr;
+ if (use && use->ref->isAttached()) {
+ label = use->ref->getObject()->getAttribute("inkscape:label");
+ if (!label || !label[0]) {
+ label = use->ref->getObject()->getId();
+ }
+ } else {
+ label = obj->getAttribute("inkscape:label");
+ }
+ row[_model->_colLabel] = label ? label : obj->getId();
+ row[_model->_colAddRemove] = SP_IS_TAG(obj);
+ }
+
+ return false;
+}
+
+void TagsPanel::_objectsSelected( Selection *sel ) {
+
+ _selectedConnection.block();
+ _tree.get_selection()->unselect_all();
+ auto tmp = sel->objects();
+ for(auto i = tmp.begin(); i != tmp.end(); ++i)
+ {
+ SPObject *obj = *i;
+ _store->foreach(sigc::bind<SPObject *>( sigc::mem_fun(*this, &TagsPanel::_checkForSelected), obj));
+ }
+ _selectedConnection.unblock();
+ _checkTreeSelection();
+}
+
+bool TagsPanel::_checkForSelected(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj)
+{
+ Gtk::TreeModel::Row row = *iter;
+ SPObject * it = row[_model->_colObject];
+ if ( it && SP_IS_TAG_USE(it) && SP_TAG_USE(it)->ref->getObject() == obj )
+ {
+ Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection();
+
+ select->select(iter);
+ }
+ return false;
+}
+
+// TODO does not look good that we're ignoring the passed in root. Investigate.
+void TagsPanel::_objectsChanged(SPObject* root)
+{
+ while (!_objectWatchers.empty())
+ {
+ TagsPanel::ObjectWatcher *w = _objectWatchers.back();
+ w->_repr->removeObserver(*w);
+ _objectWatchers.pop_back();
+ delete w;
+ }
+
+ if (_desktop) {
+ SPDocument* document = _desktop->doc();
+ SPDefs* root = document->getDefs();
+ if ( root ) {
+ _selectedConnection.block();
+ _store->clear();
+ _addObject( document, root, nullptr );
+ _selectedConnection.unblock();
+ _objectsSelected(_desktop->selection);
+ _checkTreeSelection();
+ }
+ }
+}
+
+void TagsPanel::_addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow )
+{
+ if ( _desktop && obj ) {
+ for (auto& child: obj->children) {
+ if (SP_IS_TAG(&child))
+ {
+ Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend();
+ Gtk::TreeModel::Row row = *iter;
+ row[_model->_colObject] = &child;
+ row[_model->_colParentObject] = NULL;
+ row[_model->_colLabel] = child.label() ? child.label() : child.getId();
+ row[_model->_colAddRemove] = 1;
+ row[_model->_colAllowAddRemove] = true;
+
+ _tree.expand_to_path( _store->get_path(iter) );
+
+ TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child);
+ child.getRepr()->addObserver(*w);
+ _objectWatchers.push_back(w);
+ _addObject( doc, &child, &row );
+ }
+ }
+ if (SP_IS_TAG(obj) && obj->firstChild())
+ {
+ Gtk::TreeModel::iterator iteritems = parentRow ? _store->append(parentRow->children()) : _store->prepend();
+ Gtk::TreeModel::Row rowitems = *iteritems;
+ rowitems[_model->_colObject] = NULL;
+ rowitems[_model->_colParentObject] = obj;
+ rowitems[_model->_colLabel] = _("Items");
+ rowitems[_model->_colAddRemove] = 0;
+ rowitems[_model->_colAllowAddRemove] = false;
+
+ _tree.expand_to_path( _store->get_path(iteritems) );
+
+ for (auto& child: obj->children) {
+ if (SP_IS_TAG_USE(&child))
+ {
+ SPItem *item = SP_TAG_USE(&child)->ref->getObject();
+ Gtk::TreeModel::iterator iter = _store->prepend(rowitems->children());
+ Gtk::TreeModel::Row row = *iter;
+ row[_model->_colObject] = &child;
+ row[_model->_colParentObject] = NULL;
+ row[_model->_colLabel] = item ? (item->label() ? item->label() : item->getId()) : SP_TAG_USE(&child)->href;
+ row[_model->_colAddRemove] = 0;
+ row[_model->_colAllowAddRemove] = true;
+
+ if (SP_TAG(obj)->expanded()) {
+ _tree.expand_to_path( _store->get_path(iter) );
+ }
+
+ if (item) {
+ TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child, item->getRepr());
+ item->getRepr()->addObserver(*w);
+ _objectWatchers.push_back(w);
+ }
+ }
+ }
+ }
+ }
+}
+
+void TagsPanel::_select_tag( SPTag * tag )
+{
+ for (auto& child: tag->children) {
+ if (SP_IS_TAG(&child)) {
+ _select_tag(SP_TAG(&child));
+ } else if (SP_IS_TAG_USE(&child)) {
+ SPObject * obj = SP_TAG_USE(&child)->ref->getObject();
+ if (obj) {
+ if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(obj->parent);
+ _desktop->selection->add(obj);
+ }
+ }
+ }
+}
+
+void TagsPanel::_selected_row_callback( const Gtk::TreeModel::iterator& iter )
+{
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ SPObject *obj = row[_model->_colObject];
+ if (obj) {
+ if (SP_IS_TAG(obj)) {
+ _select_tag(SP_TAG(obj));
+ } else if (SP_IS_TAG_USE(obj)) {
+ SPObject * item = SP_TAG_USE(obj)->ref->getObject();
+ if (item) {
+ if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent);
+ _desktop->selection->add(item);
+ }
+ }
+ }
+ }
+}
+
+void TagsPanel::_pushTreeSelectionToCurrent()
+{
+ _selectionChangedConnection.block();
+ // TODO hunt down the possible API abuse in getting NULL
+ if ( _desktop && _desktop->currentRoot() ) {
+ _desktop->selection->clear();
+ _tree.get_selection()->selected_foreach_iter( sigc::mem_fun(*this, &TagsPanel::_selected_row_callback));
+ }
+ _selectionChangedConnection.unblock();
+
+ _checkTreeSelection();
+}
+
+void TagsPanel::_checkTreeSelection()
+{
+ bool sensitive = _tree.get_selection()->count_selected_rows() > 0;
+ bool sensitiveNonTop = true;
+ bool sensitiveNonBottom = true;
+// if ( _tree.get_selection()->count_selected_rows() > 0 ) {
+// sensitive = true;
+//
+// SPObject* inTree = _selectedLayer();
+// if ( inTree ) {
+//
+// sensitiveNonTop = (Inkscape::Nex(inTree->parent, inTree) != 0);
+// sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != 0);
+//
+// }
+// }
+
+
+ for (auto & it : _watching) {
+ it->set_sensitive( sensitive );
+ }
+ for (auto & it : _watchingNonTop) {
+ it->set_sensitive( sensitiveNonTop );
+ }
+ for (auto & it : _watchingNonBottom) {
+ it->set_sensitive( sensitiveNonBottom );
+ }
+}
+
+bool TagsPanel::_handleKeyEvent(GdkEventKey *event)
+{
+
+ switch (Inkscape::UI::Tools::get_latin_keyval(event)) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ case GDK_KEY_F2: {
+ Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected();
+ if (iter && !_text_renderer->property_editable()) {
+ Gtk::TreeRow row = *iter;
+ SPObject * obj = row[_model->_colObject];
+ if (obj && SP_IS_TAG(obj)) {
+ Gtk::TreeModel::Path *path = new Gtk::TreeModel::Path(iter);
+ // Edit the layer label
+ _text_renderer->property_editable() = true;
+ _tree.set_cursor(*path, *_name_column, true);
+ grab_focus();
+ return true;
+ }
+ }
+ }
+ case GDK_KEY_Delete: {
+ std::vector<SPObject *> todelete;
+ _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete));
+ if (!todelete.empty()) {
+ for (auto obj : todelete) {
+ if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) {
+ //obj->parent->getRepr()->removeChild(obj->getRepr());
+ obj->deleteObject(true, true);
+ }
+ }
+ DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set"));
+ }
+ return true;
+ }
+ break;
+ }
+ return false;
+}
+
+bool TagsPanel::_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<int>(event->x);
+ int y = static_cast<int>(event->y);
+ if ( _tree.get_path_at_pos( x, y, path ) ) {
+ _checkTreeSelection();
+#if GTKMM_CHECK_VERSION(3,22,0)
+ _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+#else
+ _popupMenu.popup(event->button, event->time);
+#endif
+ if (_tree.get_selection()->is_selected(path)) {
+ return true;
+ }
+ }
+ }
+
+ if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) {
+ // 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<int>(event->x);
+ int y = static_cast<int>(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_ADD-1)) {
+ down_at_add = true;
+ return true;
+ } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) {
+ _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_noSelection));
+ _defer_target = path;
+ } else {
+ down_at_add = false;
+ }
+ } else {
+ down_at_add = false;
+ }
+ }
+
+ if ( event->type == GDK_BUTTON_RELEASE) {
+ _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction));
+ }
+
+ // 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)) {
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x = static_cast<int>(event->x);
+ int y = static_cast<int>(event->y);
+ int x2 = 0;
+ int y2 = 0;
+ if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) {
+ if (_defer_target) {
+ if (_defer_target == path && !(event->x == 0 && event->y == 0))
+ {
+ _tree.set_cursor(path, *col, false);
+ }
+ _defer_target = Gtk::TreeModel::Path();
+ } else {
+ Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+
+ SPObject* obj = row[_model->_colObject];
+
+ if (obj) {
+ if (col == _tree.get_column(COL_ADD - 1) && down_at_add) {
+ if (SP_IS_TAG(obj)) {
+ bool wasadded = false;
+ auto items= _desktop->selection->items();
+ for(auto i=items.begin();i!=items.end();++i){
+ SPObject *newobj = *i;
+ bool addchild = true;
+ for (auto& child: obj->children) {
+ if (SP_IS_TAG_USE(&child) && SP_TAG_USE(&child)->ref->getObject() == newobj) {
+ addchild = false;
+ }
+ }
+ if (addchild) {
+ Inkscape::XML::Node *clone = _document->getReprDoc()->createElement("inkscape:tagref");
+ clone->setAttribute("xlink:href", g_strdup_printf("#%s", newobj->getRepr()->attribute("id")), false);
+ obj->appendChild(clone);
+ wasadded = true;
+ }
+ }
+ if (wasadded) {
+ DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Add selection to set"));
+ }
+ } else {
+ std::vector<SPObject *> todelete;
+ // FIXME unnecessary use of XML tree
+ _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete));
+ if (!todelete.empty()) {
+ for (auto tobj : todelete) {
+ if (tobj && tobj->parent && tobj->getRepr() && tobj->parent->getRepr()) {
+ //tobj->parent->getRepr()->removeChild(tobj->getRepr());
+ tobj->deleteObject(true, true);
+ }
+ }
+ } else if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) {
+ obj->parent->getRepr()->removeChild(obj->getRepr());
+ }
+ DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set"));
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) {
+ doubleclick = 1;
+ }
+
+ if ( event->type == GDK_BUTTON_RELEASE && doubleclick) {
+ doubleclick = 0;
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x = static_cast<int>(event->x);
+ int y = static_cast<int>(event->y);
+ int x2 = 0;
+ int y2 = 0;
+ if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) {
+ Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+
+ SPObject* obj = row[_model->_colObject];
+ if (obj && (SP_IS_TAG(obj) || (SP_IS_TAG_USE(obj) && SP_TAG_USE(obj)->ref->getObject()))) {
+ // Double click on the Layer name, enable editing
+ _text_renderer->property_editable() = true;
+ _tree.set_cursor (path, *_name_column, true);
+ grab_focus();
+ }
+ }
+ }
+
+ return false;
+}
+
+void TagsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter)
+{
+ Gtk::TreeModel::Row row = *iter;
+ SPObject* obj = row[_model->_colObject];
+ SPTag* item = ( obj && SP_IS_TAG(obj) ) ? SP_TAG(obj) : nullptr;
+ if (item)
+ {
+ _dnd_source.push_back(item);
+ }
+}
+
+/*
+ * 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 TagsPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/)
+{
+ int cell_x = 0, cell_y = 0;
+ Gtk::TreeModel::Path target_path;
+ Gtk::TreeView::Column *target_column;
+
+ _dnd_into = true;
+ _dnd_target = _document->getDefs();
+ _dnd_source.clear();
+ _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &TagsPanel::_storeDragSource));
+
+ if (_dnd_source.empty()) {
+ return true;
+ }
+
+ 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 = _document->getDefs();
+ _dnd_into = true;
+ }
+ }
+ }
+ 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];
+ SPObject *pobj = row[_model->_colParentObject];
+ if (obj) {
+ if (SP_IS_TAG(obj)) {
+ _dnd_target = SP_TAG(obj);
+ } else if (SP_IS_TAG(obj->parent)) {
+ _dnd_target = SP_TAG(obj->parent);
+ _dnd_into = true;
+ }
+ } else if (pobj && SP_IS_TAG(pobj)) {
+ _dnd_target = SP_TAG(pobj);
+ _dnd_into = true;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ _takeAction(DRAGNDROP);
+
+ return false;
+}
+
+/*
+ * Move a layer in response to a drag & drop action
+ */
+void TagsPanel::_doTreeMove( )
+{
+ if (_dnd_target) {
+ for (auto src : _dnd_source)
+ {
+ if (src != _dnd_target) {
+ src->moveTo(_dnd_target, _dnd_into);
+ }
+ }
+ _desktop->selection->clear();
+ // moveTo may have deleted src pointers, don't use them anymore
+ _dnd_source.clear();
+ DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_TAGS,
+ _("Moved sets"));
+ }
+}
+
+
+void TagsPanel::_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;
+}
+
+void TagsPanel::_handleEditingCancelled()
+{
+ _text_renderer->property_editable() = false;
+}
+
+void TagsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name)
+{
+ if ( row && _desktop) {
+ SPObject* obj = row[_model->_colObject];
+ if ( obj ) {
+ if (SP_IS_TAG(obj)) {
+ gchar const* oldLabel = obj->label();
+ if ( !name.empty() && (!oldLabel || name != oldLabel) ) {
+ obj->setLabel(name.c_str());
+ DocumentUndo::done( _desktop->doc() , SP_VERB_NONE,
+ _("Rename object"));
+ }
+ } else if (SP_IS_TAG_USE(obj) && (obj = SP_TAG_USE(obj)->ref->getObject())) {
+ gchar const* oldLabel = obj->label();
+ if ( !name.empty() && (!oldLabel || name != oldLabel) ) {
+ obj->setLabel(name.c_str());
+ DocumentUndo::done( _desktop->doc() , SP_VERB_NONE,
+ _("Rename object"));
+ }
+ }
+ }
+ }
+}
+
+bool TagsPanel::_noSelection( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ )
+{
+ return false;
+}
+
+bool TagsPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> 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<GdkEventButton const*>(_toggleEvent);
+ GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(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;
+}
+
+void TagsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded)
+{
+ Gtk::TreeModel::Row row = *iter;
+
+ SPObject* obj = row[_model->_colParentObject];
+ if (obj && SP_IS_TAG(obj))
+ {
+ SP_TAG(obj)->setExpanded(isexpanded);
+ obj->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ }
+}
+
+/**
+ * Constructor
+ */
+TagsPanel::TagsPanel() :
+ UI::Widget::Panel("/dialogs/tags", SP_VERB_DIALOG_TAGS),
+ _rootWatcher(nullptr),
+ deskTrack(),
+ _desktop(nullptr),
+ _document(nullptr),
+ _model(nullptr),
+ _pending(nullptr),
+ _toggleEvent(nullptr),
+ _defer_target(),
+ desktopChangeConn()
+{
+ //Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ 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);
+
+ // This string is constructed to use already translated strings.
+ // The tooltip applies to the whole tree area. It would be better
+ // if the tooltip was split into parts and only applied to the
+ // icons but doing that is quite complicated.
+ Glib::ustring tooltip_string = "'+': ";
+ tooltip_string += (_("Add selection to set"));
+ tooltip_string += "; '×': ";
+ tooltip_string += (_("Remove from selection set"));
+ _tree.set_tooltip_text( tooltip_string );
+
+ Inkscape::UI::Widget::IconRenderer * addRenderer = manage( new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ addRenderer->add_icon("list-add");
+
+ int addColNum = _tree.append_column("type", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = _tree.get_column(addColNum);
+ if ( col ) {
+ col->add_attribute( addRenderer->property_icon(), _model->_colAddRemove );
+ col->add_attribute( addRenderer->property_visible(), _model->_colAllowAddRemove );
+ }
+
+ _text_renderer = 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.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE);
+ _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &TagsPanel::_pushTreeSelectionToCurrent) );
+ _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction) );
+
+ _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &TagsPanel::_handleDragDrop), false);
+ _collapsedConnection = _tree.signal_row_collapsed().connect( sigc::bind<bool>(sigc::mem_fun(*this, &TagsPanel::_setExpanded), false));
+ _expandedConnection = _tree.signal_row_expanded().connect( sigc::bind<bool>(sigc::mem_fun(*this, &TagsPanel::_setExpanded), true));
+
+ _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &TagsPanel::_handleEdited) );
+ _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &TagsPanel::_handleEditingCancelled) );
+
+ _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false );
+ _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false );
+ _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleKeyEvent), 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);
+ }
+
+ _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET );
+
+ _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK);
+
+ _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET);
+
+ SPDesktop* targetDesktop = getDesktop();
+
+ Gtk::Button* btn = manage( new Gtk::Button() );
+ _styleButton(*btn, "list-add", _("Add a new selection set") );
+ btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_takeAction), (int)BUTTON_NEW) );
+ _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK);
+
+// btn = manage( new Gtk::Button("Dup") );
+// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) );
+// _buttonsRow.add( *btn );
+
+ btn = manage( new Gtk::Button() );
+ _styleButton( *btn, "list-remove", _("Remove Item/Set") );
+ btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_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);
+
+ // -------------------------------------------------------
+ {
+ _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_TAG_NEW, nullptr, "Add a new selection set", (int)BUTTON_NEW ) );
+
+ _popupMenu.show_all_children();
+ }
+ // -------------------------------------------------------
+
+
+
+ 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, &TagsPanel::setDesktop) );
+ deskTrack.connect(GTK_WIDGET(gobj()));
+}
+
+TagsPanel::~TagsPanel()
+{
+
+ 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();
+}
+
+void TagsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document)
+{
+ while (!_objectWatchers.empty())
+ {
+ TagsPanel::ObjectWatcher *w = _objectWatchers.back();
+ w->_repr->removeObserver(*w);
+ _objectWatchers.pop_back();
+ delete w;
+ }
+
+ if (_rootWatcher)
+ {
+ _rootWatcher->_repr->removeObserver(*_rootWatcher);
+ delete _rootWatcher;
+ _rootWatcher = nullptr;
+ }
+
+ _document = document;
+
+ if (document && document->getDefs() && document->getDefs()->getRepr())
+ {
+ _rootWatcher = new TagsPanel::ObjectWatcher(this, document->getDefs());
+ document->getDefs()->getRepr()->addObserver(*_rootWatcher);
+ _objectsChanged(document->getDefs());
+ }
+}
+
+void TagsPanel::setDesktop( SPDesktop* desktop )
+{
+ Panel::setDesktop(desktop);
+
+ if ( desktop != _desktop ) {
+ _documentChangedConnection.disconnect();
+ _selectionChangedConnection.disconnect();
+ if ( _desktop ) {
+ _desktop = nullptr;
+ }
+
+ _desktop = Panel::getDesktop();
+ if ( _desktop ) {
+ //setLabel( _desktop->doc()->name );
+ _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &TagsPanel::setDocument));
+ _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &TagsPanel::_objectsSelected));
+
+ setDocument(_desktop, _desktop->doc());
+ }
+ }
+ 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/tags.h b/src/ui/dialog/tags.h
new file mode 100644
index 0000000..f8f1215
--- /dev/null
+++ b/src/ui/dialog/tags.h
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple dialog for tags UI.
+ *
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_TAGS_PANEL_H
+#define SEEN_TAGS_PANEL_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/dialog.h>
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/panel.h"
+#include "object/sp-tag.h"
+#include "object/sp-tag-use.h"
+#include "object/sp-tag-use-reference.h"
+#include "desktop-tracker.h"
+#include "selection.h"
+#include "ui/widget/filter-effect-chooser.h"
+
+class SPObject;
+class SPTag;
+struct SPColorSelector;
+
+namespace Inkscape {
+
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * A panel that displays layers.
+ */
+class TagsPanel : public UI::Widget::Panel
+{
+public:
+ TagsPanel();
+ ~TagsPanel() override;
+
+ static TagsPanel& getInstance();
+
+ void setDesktop( SPDesktop* desktop ) override;
+ void setDocument( SPDesktop* desktop, SPDocument* document);
+
+protected:
+ friend void sp_highlight_picker_color_mod(SPColorSelector *csel, GObject *cp);
+private:
+ class ModelColumns;
+ class InternalUIBounce;
+ class ObjectWatcher;
+
+ TagsPanel(TagsPanel const &) = delete; // no copy
+ TagsPanel &operator=(TagsPanel 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, char const* iconName, char const* fallback, int id );
+
+ bool _handleButtonEvent(GdkEventButton *event);
+ bool _handleKeyEvent(GdkEventKey *event);
+
+ void _storeDragSource(const Gtk::TreeModel::iterator& iter);
+ bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& 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();
+ void _selected_row_callback( const Gtk::TreeModel::iterator& iter );
+ void _select_tag( SPTag * tag );
+
+ void _checkTreeSelection();
+
+ void _takeAction( int val );
+ bool _executeAction();
+
+ void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded );
+
+ bool _noSelection( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b );
+ bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b );
+
+ void _updateObject(SPObject *obj);
+ bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* obj);
+
+ void _objectsSelected(Selection *sel);
+ bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer);
+
+ void _objectsChanged(SPObject *root);
+ void _addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow );
+
+ void _checkForDeleted(const Gtk::TreeIter& iter, std::vector<SPObject *>* todelete);
+
+// std::vector<sigc::connection> groupConnections;
+ TagsPanel::ObjectWatcher* _rootWatcher;
+ std::vector<TagsPanel::ObjectWatcher*> _objectWatchers;
+
+ // Hooked to the layer manager:
+ sigc::connection _documentChangedConnection;
+ sigc::connection _selectionChangedConnection;
+
+ sigc::connection _changedConnection;
+ sigc::connection _addedConnection;
+ sigc::connection _removedConnection;
+
+ // Internal
+ sigc::connection _selectedConnection;
+ sigc::connection _expandedConnection;
+ sigc::connection _collapsedConnection;
+
+ DesktopTracker deskTrack;
+ SPDesktop* _desktop;
+ SPDocument* _document;
+ ModelColumns* _model;
+ InternalUIBounce* _pending;
+ gboolean _dnd_into;
+ std::vector<SPTag*> _dnd_source;
+ SPObject* _dnd_target;
+
+ GdkEvent* _toggleEvent;
+ bool down_at_add;
+
+ Gtk::TreeModel::Path _defer_target;
+
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ std::vector<Gtk::Widget*> _watching;
+ std::vector<Gtk::Widget*> _watchingNonTop;
+ std::vector<Gtk::Widget*> _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;
+
+ sigc::connection desktopChangeConn;
+
+};
+
+
+
+} //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/template-load-tab.cpp b/src/ui/dialog/template-load-tab.cpp
new file mode 100644
index 0000000..6f5c654
--- /dev/null
+++ b/src/ui/dialog/template-load-tab.cpp
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template abstract tab implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof KosiƄski
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-widget.h"
+#include "new-from-template.h"
+
+#include <glibmm/miscutils.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/fileutils.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/scrolledwindow.h>
+#include <iostream>
+
+#include "extension/extension.h"
+#include "extension/db.h"
+#include "inkscape.h"
+#include "file.h"
+#include "path-prefix.h"
+
+using namespace Inkscape::IO::Resource;
+
+namespace Inkscape {
+namespace UI {
+
+TemplateLoadTab::TemplateLoadTab(NewFromTemplate* parent)
+ : _current_keyword("")
+ , _keywords_combo(true)
+ , _current_search_type(ALL)
+ , _parent_widget(parent)
+{
+ set_border_width(10);
+
+ _info_widget = Gtk::manage(new TemplateWidget());
+
+ Gtk::Label *title;
+ title = Gtk::manage(new Gtk::Label(_("Search:")));
+ _search_box.pack_start(*title, Gtk::PACK_SHRINK);
+ _search_box.pack_start(_keywords_combo, Gtk::PACK_SHRINK, 5);
+
+ _tlist_box.pack_start(_search_box, Gtk::PACK_SHRINK, 10);
+
+ pack_start(_tlist_box, Gtk::PACK_SHRINK);
+ pack_start(*_info_widget, Gtk::PACK_EXPAND_WIDGET, 5);
+
+ Gtk::ScrolledWindow *scrolled;
+ scrolled = Gtk::manage(new Gtk::ScrolledWindow());
+ scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled->add(_tlist_view);
+ _tlist_box.pack_start(*scrolled, Gtk::PACK_EXPAND_WIDGET, 5);
+
+ _keywords_combo.signal_changed().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_keywordSelected));
+ this->show_all();
+
+ _loadTemplates();
+ _initLists();
+}
+
+
+TemplateLoadTab::~TemplateLoadTab()
+= default;
+
+
+void TemplateLoadTab::createTemplate()
+{
+ _info_widget->create();
+}
+
+
+void TemplateLoadTab::_onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*)
+{
+ createTemplate();
+ NewFromTemplate* parent = static_cast<NewFromTemplate*> (this->get_toplevel());
+ parent->_onClose();
+}
+
+void TemplateLoadTab::_displayTemplateInfo()
+{
+ Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef = _tlist_view.get_selection();
+ if (templateSelectionRef->get_selected()) {
+ _current_template = (*templateSelectionRef->get_selected())[_columns.textValue];
+
+ _info_widget->display(_tdata[_current_template]);
+ _parent_widget->setCreateButtonSensitive(true);
+ }
+
+}
+
+
+void TemplateLoadTab::_initKeywordsList()
+{
+ _keywords_combo.append(_("All"));
+
+ for (const auto & _keyword : _keywords){
+ _keywords_combo.append(_keyword);
+ }
+}
+
+
+void TemplateLoadTab::_initLists()
+{
+ _tlist_store = Gtk::ListStore::create(_columns);
+ _tlist_view.set_model(_tlist_store);
+ _tlist_view.append_column("", _columns.textValue);
+ _tlist_view.set_headers_visible(false);
+
+ _initKeywordsList();
+ _refreshTemplatesList();
+
+ Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef =
+ _tlist_view.get_selection();
+ templateSelectionRef->signal_changed().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_displayTemplateInfo));
+
+ _tlist_view.signal_row_activated().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_onRowActivated));
+}
+
+void TemplateLoadTab::_keywordSelected()
+{
+ _current_keyword = _keywords_combo.get_active_text();
+ if (_current_keyword == ""){
+ _current_keyword = _keywords_combo.get_entry_text();
+ _current_search_type = USER_SPECIFIED;
+ }
+ else
+ _current_search_type = LIST_KEYWORD;
+
+ if (_current_keyword == "" || _current_keyword == _("All"))
+ _current_search_type = ALL;
+
+ _refreshTemplatesList();
+}
+
+
+void TemplateLoadTab::_refreshTemplatesList()
+{
+ _tlist_store->clear();
+
+ switch (_current_search_type){
+ case ALL :{
+ for (auto & it : _tdata) {
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ break;
+ }
+
+ case LIST_KEYWORD: {
+ for (auto & it : _tdata) {
+ if (it.second.keywords.count(_current_keyword.lowercase()) != 0){
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ }
+ break;
+ }
+
+ case USER_SPECIFIED : {
+ for (auto & it : _tdata) {
+ if (it.second.keywords.count(_current_keyword.lowercase()) != 0 ||
+ it.second.display_name.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos ||
+ it.second.author.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos ||
+ it.second.short_description.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos)
+ {
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ }
+ break;
+ }
+ }
+
+ // reselect item
+ Gtk::TreeIter* item_to_select = nullptr;
+ for (Gtk::TreeModel::Children::iterator it = _tlist_store->children().begin(); it != _tlist_store->children().end(); ++it) {
+ Gtk::TreeModel::Row row = *it;
+ if (_current_template == row[_columns.textValue]) {
+ item_to_select = new Gtk::TreeIter(it);
+ break;
+ }
+ }
+ if (_tlist_store->children().size() == 1) {
+ delete item_to_select;
+ item_to_select = new Gtk::TreeIter(_tlist_store->children().begin());
+ }
+ if (item_to_select) {
+ _tlist_view.get_selection()->select(*item_to_select);
+ delete item_to_select;
+ } else {
+ _current_template = "";
+ _info_widget->clear();
+ _parent_widget->setCreateButtonSensitive(false);
+ }
+}
+
+
+void TemplateLoadTab::_loadTemplates()
+{
+ for(auto &filename: get_filenames(TEMPLATES, {".svg"}, {"default."})) {
+ TemplateData tmp = _processTemplateFile(filename);
+ if (tmp.display_name != "")
+ _tdata[tmp.display_name] = tmp;
+
+ }
+ // procedural templates
+ _getProceduralTemplates();
+}
+
+
+TemplateLoadTab::TemplateData TemplateLoadTab::_processTemplateFile(const std::string &path)
+{
+ TemplateData result;
+ result.path = path;
+ result.is_procedural = false;
+ result.preview_name = "";
+
+ // convert path into valid template name
+ result.display_name = Glib::path_get_basename(path);
+ gsize n = 0;
+ while ((n = result.display_name.find_first_of("_", 0)) < Glib::ustring::npos){
+ result.display_name.replace(n, 1, 1, ' ');
+ }
+ n = result.display_name.rfind(".svg");
+ result.display_name.replace(n, 4, 1, ' ');
+
+ Inkscape::XML::Document *rdoc = sp_repr_read_file(path.data(), SP_SVG_NS_URI);
+ if (rdoc){
+ Inkscape::XML::Node *root = rdoc->root();
+ if (strcmp(root->name(), "svg:svg") != 0){ // Wrong file format
+ return result;
+ }
+
+ Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(root, "inkscape:templateinfo");
+ if (!templateinfo) {
+ templateinfo = sp_repr_lookup_name(root, "inkscape:_templateinfo"); // backwards-compatibility
+ }
+
+ if (templateinfo == nullptr) // No template info
+ return result;
+ _getDataFromNode(templateinfo, result);
+ }
+
+ return result;
+}
+
+void TemplateLoadTab::_getProceduralTemplates()
+{
+ std::list<Inkscape::Extension::Effect *> effects;
+ Inkscape::Extension::db.get_effect_list(effects);
+
+ std::list<Inkscape::Extension::Effect *>::iterator it = effects.begin();
+ while (it != effects.end()){
+ Inkscape::XML::Node *repr = (*it)->get_repr();
+ Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(repr, "inkscape:templateinfo");
+ if (!templateinfo) {
+ templateinfo = sp_repr_lookup_name(repr, "inkscape:_templateinfo"); // backwards-compatibility
+ }
+
+ if (templateinfo){
+ TemplateData result;
+ result.display_name = (*it)->get_name();
+ result.is_procedural = true;
+ result.path = "";
+ result.tpl_effect = *it;
+
+ _getDataFromNode(templateinfo, result, *it);
+ _tdata[result.display_name] = result;
+ }
+ ++it;
+ }
+}
+
+// if the template data comes from a procedural template (aka Effect extension),
+// attempt to translate within the extension's context (which might use a different gettext textdomain)
+const char *_translate(const char* msgid, Extension::Extension *extension)
+{
+ if (extension) {
+ return extension->get_translation(msgid);
+ } else {
+ return _(msgid);
+ }
+}
+
+void TemplateLoadTab::_getDataFromNode(Inkscape::XML::Node *dataNode, TemplateData &data, Extension::Extension *extension)
+{
+ Inkscape::XML::Node *currentData;
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:name")) != nullptr)
+ data.display_name = _translate(currentData->firstChild()->content(), extension);
+ else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_name")) != nullptr) // backwards-compatibility
+ data.display_name = _translate(currentData->firstChild()->content(), extension);
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:author")) != nullptr)
+ data.author = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:shortdesc")) != nullptr)
+ data.short_description = _translate(currentData->firstChild()->content(), extension);
+ else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_shortdesc")) != nullptr) // backwards-compatibility
+ data.short_description = _translate(currentData->firstChild()->content(), extension);
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:preview")) != nullptr)
+ data.preview_name = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:date")) != nullptr)
+ data.creation_date = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_keywords")) != nullptr){
+ Glib::ustring tplKeywords = _translate(currentData->firstChild()->content(), extension);
+ while (!tplKeywords.empty()){
+ std::size_t pos = tplKeywords.find_first_of(" ");
+ if (pos == Glib::ustring::npos)
+ pos = tplKeywords.size();
+
+ Glib::ustring keyword = tplKeywords.substr(0, pos).data();
+ data.keywords.insert(keyword.lowercase());
+ _keywords.insert(keyword.lowercase());
+
+ if (pos == tplKeywords.size())
+ break;
+ tplKeywords.erase(0, pos+1);
+ }
+ }
+}
+
+}
+}
diff --git a/src/ui/dialog/template-load-tab.h b/src/ui/dialog/template-load-tab.h
new file mode 100644
index 0000000..2b8f98f
--- /dev/null
+++ b/src/ui/dialog/template-load-tab.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template abstract tab class
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, 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_TEMPLATE_LOAD_TAB_H
+#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <map>
+#include <set>
+#include <string>
+
+#include "xml/node.h"
+#include "io/resource.h"
+#include "extension/effect.h"
+
+
+namespace Inkscape {
+
+namespace Extension {
+class Extension;
+}
+
+namespace UI {
+
+class TemplateWidget;
+class NewFromTemplate;
+
+class TemplateLoadTab : public Gtk::HBox
+{
+
+public:
+ struct TemplateData
+ {
+ bool is_procedural;
+ std::string path;
+ Glib::ustring display_name;
+ Glib::ustring author;
+ Glib::ustring short_description;
+ Glib::ustring long_description; // unused
+ Glib::ustring preview_name;
+ Glib::ustring creation_date;
+ std::set<Glib::ustring> keywords;
+ Inkscape::Extension::Effect *tpl_effect;
+ };
+
+ TemplateLoadTab(NewFromTemplate* parent);
+ ~TemplateLoadTab() override;
+ virtual void createTemplate();
+
+protected:
+ class StringModelColumns : public Gtk::TreeModelColumnRecord
+ {
+ public:
+ StringModelColumns()
+ {
+ add(textValue);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> textValue;
+ };
+
+ Glib::ustring _current_keyword;
+ Glib::ustring _current_template;
+ std::map<Glib::ustring, TemplateData> _tdata;
+ std::set<Glib::ustring> _keywords;
+
+
+ virtual void _displayTemplateInfo();
+ virtual void _initKeywordsList();
+ virtual void _refreshTemplatesList();
+ void _loadTemplates();
+ void _initLists();
+
+ Gtk::VBox _tlist_box;
+ Gtk::HBox _search_box;
+ TemplateWidget *_info_widget;
+
+ Gtk::ComboBoxText _keywords_combo;
+
+ Gtk::TreeView _tlist_view;
+ Glib::RefPtr<Gtk::ListStore> _tlist_store;
+ StringModelColumns _columns;
+
+private:
+ enum SearchType
+ {
+ LIST_KEYWORD,
+ USER_SPECIFIED,
+ ALL
+ };
+
+ SearchType _current_search_type;
+ NewFromTemplate* _parent_widget;
+
+ void _getDataFromNode(Inkscape::XML::Node *, TemplateData &, Extension::Extension *extension=nullptr);
+ void _getProceduralTemplates();
+ void _getTemplatesFromDomain(Inkscape::IO::Resource::Domain domain);
+ void _keywordSelected();
+ TemplateData _processTemplateFile(const std::string &);
+
+ void _onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*);
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/dialog/template-widget.cpp b/src/ui/dialog/template-widget.cpp
new file mode 100644
index 0000000..c5c4522
--- /dev/null
+++ b/src/ui/dialog/template-widget.cpp
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template - templates widget - implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof KosiƄski
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-widget.h"
+
+#include <glibmm/miscutils.h>
+#include <gtkmm/messagedialog.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "file.h"
+#include "inkscape.h"
+
+#include "extension/implementation/implementation.h"
+
+#include "object/sp-namedview.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+TemplateWidget::TemplateWidget()
+ : _more_info_button(_("More info"))
+ , _short_description_label(" ")
+ , _template_name_label(_("no template selected"))
+ , _effect_prefs(nullptr)
+{
+ pack_start(_template_name_label, Gtk::PACK_SHRINK, 10);
+ pack_start(_preview_box, Gtk::PACK_SHRINK, 0);
+
+ _preview_box.pack_start(_preview_image, Gtk::PACK_EXPAND_PADDING, 15);
+ _preview_box.pack_start(_preview_render, Gtk::PACK_EXPAND_PADDING, 10);
+
+ _short_description_label.set_line_wrap(true);
+
+ _more_info_button.set_halign(Gtk::ALIGN_END);
+ _more_info_button.set_valign(Gtk::ALIGN_CENTER);
+ pack_end(_more_info_button, Gtk::PACK_SHRINK);
+
+ pack_end(_short_description_label, Gtk::PACK_SHRINK, 5);
+
+ _more_info_button.signal_clicked().connect(
+ sigc::mem_fun(*this, &TemplateWidget::_displayTemplateDetails));
+ _more_info_button.set_sensitive(false);
+}
+
+
+void TemplateWidget::create()
+{
+ if (_current_template.display_name == "")
+ return;
+
+ if (_current_template.is_procedural){
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDesktop *desc = sp_file_new_default();
+ _current_template.tpl_effect->effect(desc);
+ DocumentUndo::clearUndo(desc->getDocument());
+ desc->getDocument()->setModifiedSinceSave(false);
+
+ // Apply cx,cy etc. from document
+ sp_namedview_window_from_document( desc );
+
+ if (desktop)
+ desktop->clearWaitingCursor();
+ }
+ else {
+ sp_file_new(_current_template.path);
+ }
+}
+
+
+void TemplateWidget::display(TemplateLoadTab::TemplateData data)
+{
+ clear();
+ _current_template = data;
+
+ _template_name_label.set_text(_current_template.display_name);
+ _short_description_label.set_text(_current_template.short_description);
+
+ if (data.preview_name != ""){
+ std::string imagePath = Glib::build_filename(Glib::path_get_dirname(_current_template.path), _current_template.preview_name);
+ _preview_image.set(imagePath);
+ _preview_image.show();
+ }
+ else if (!data.is_procedural){
+ Glib::ustring gPath = data.path.c_str();
+ _preview_render.showImage(gPath);
+ _preview_render.show();
+ }
+
+ if (data.is_procedural){
+ _effect_prefs = data.tpl_effect->get_imp()->prefs_effect(data.tpl_effect, SP_ACTIVE_DESKTOP, nullptr, nullptr);
+ pack_start(*_effect_prefs);
+ }
+ _more_info_button.set_sensitive(true);
+}
+
+void TemplateWidget::clear()
+{
+ _template_name_label.set_text("");
+ _short_description_label.set_text("");
+ _preview_render.hide();
+ _preview_image.hide();
+ if (_effect_prefs != nullptr){
+ remove (*_effect_prefs);
+ _effect_prefs = nullptr;
+ }
+ _more_info_button.set_sensitive(false);
+}
+
+void TemplateWidget::_displayTemplateDetails()
+{
+ Glib::ustring message = _current_template.display_name + "\n\n";
+
+ if (!_current_template.author.empty()) {
+ message += _("Author");
+ message += ": ";
+ message += _current_template.author + " " + _current_template.creation_date + "\n\n";
+ }
+
+ if (!_current_template.keywords.empty()){
+ message += _("Keywords");
+ message += ":";
+ for (const auto & keyword : _current_template.keywords) {
+ message += " ";
+ message += keyword;
+ }
+ message += "\n\n";
+ }
+
+ if (!_current_template.path.empty()) {
+ message += _("Path");
+ message += ": ";
+ message += _current_template.path;
+ message += "\n\n";
+ }
+
+ Gtk::MessageDialog dl(message, false, Gtk::MESSAGE_OTHER);
+ dl.run();
+}
+
+}
+}
diff --git a/src/ui/dialog/template-widget.h b/src/ui/dialog/template-widget.h
new file mode 100644
index 0000000..5d7023b
--- /dev/null
+++ b/src/ui/dialog/template-widget.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template - template widget
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, 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_TEMPLATE_WIDGET_H
+#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H
+
+#include "svg-preview.h"
+
+#include <gtkmm/box.h>
+
+#include "template-load-tab.h"
+
+
+namespace Inkscape {
+namespace UI {
+
+
+class TemplateWidget : public Gtk::VBox
+{
+public:
+ TemplateWidget ();
+ void create();
+ void display(TemplateLoadTab::TemplateData);
+ void clear();
+
+private:
+ TemplateLoadTab::TemplateData _current_template;
+
+ Gtk::Button _more_info_button;
+ Gtk::HBox _preview_box;
+ Gtk::Image _preview_image;
+ Dialog::SVGPreview _preview_render;
+ Gtk::Label _short_description_label;
+ Gtk::Label _template_name_label;
+ Gtk::Widget *_effect_prefs;
+
+ void _displayTemplateDetails();
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp
new file mode 100644
index 0000000..652798a
--- /dev/null
+++ b/src/ui/dialog/text-edit.cpp
@@ -0,0 +1,585 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Text editing dialog.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Abhishek Sharma
+ * John Smith
+ * Tavmjong Bah
+ *
+ * Copyright (C) 1999-2013 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * 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 "text-edit.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#ifdef WITH_GTKSPELL
+extern "C" {
+# include <gtkspell/gtkspell.h>
+}
+#endif
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "text-editing.h"
+#include "verbs.h"
+
+#include <libnrtype/FontFactory.h>
+#include <libnrtype/font-instance.h>
+#include <libnrtype/font-lister.h>
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+
+#include "svg/css-ostringstream.h"
+#include "ui/icon-names.h"
+#include "ui/toolbar/text-toolbar.h"
+#include "ui/widget/font-selector.h"
+
+#include "util/units.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+TextEdit::TextEdit()
+ : UI::Widget::Panel("/dialogs/textandfont", SP_VERB_DIALOG_TEXT),
+ font_label(_("_Font"), true),
+ text_label(_("_Text"), true),
+ feat_label(_("_Features"), true),
+ setasdefault_button(_("Set as _default")),
+ close_button(_("_Close"), true),
+ apply_button(_("_Apply"), true),
+ desktop(nullptr),
+ deskTrack(),
+ selectChangedConn(),
+ subselChangedConn(),
+ selectModifiedConn(),
+ blocked(false),
+ /*
+ TRANSLATORS: Test string used in text and font dialog (when no
+ * text has been entered) to get a preview of the font. Choose
+ * some representative characters that users of your locale will be
+ * interested in.*/
+ samplephrase(_("AaBbCcIiPpQq12369$\342\202\254\302\242?.;/()"))
+{
+
+ /* Font tab -------------------------------- */
+
+ /* Font selector */
+ // Do nothing.
+
+ /* Font preview */
+ preview_label.set_ellipsize (Pango::ELLIPSIZE_END);
+ preview_label.set_justify (Gtk::JUSTIFY_CENTER);
+ preview_label.set_line_wrap (false);
+
+ font_vbox.set_border_width(4);
+ font_vbox.pack_start(font_selector, true, true);
+ font_vbox.pack_start(preview_label, false, false, 4);
+
+ /* Features tab ---------------------------- */
+
+ /* Features preview */
+ preview_label2.set_ellipsize (Pango::ELLIPSIZE_END);
+ preview_label2.set_justify (Gtk::JUSTIFY_CENTER);
+ preview_label2.set_line_wrap (false);
+
+ feat_vbox.set_border_width(4);
+ feat_vbox.pack_start(font_features, true, true);
+ feat_vbox.pack_start(preview_label2, false, false, 4);
+
+ /* Text tab -------------------------------- */
+ scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ scroller.set_shadow_type(Gtk::SHADOW_IN);
+
+ text_buffer = gtk_text_buffer_new (nullptr);
+ text_view = gtk_text_view_new_with_buffer (text_buffer);
+ gtk_text_view_set_wrap_mode ((GtkTextView *) text_view, GTK_WRAP_WORD);
+
+#ifdef WITH_GTKSPELL
+ /*
+ TODO: Use computed xml:lang attribute of relevant element, if present, to specify the
+ language (either as 2nd arg of gtkspell_new_attach, or with explicit
+ gtkspell_set_language call in; see advanced.c example in gtkspell docs).
+ onReadSelection looks like a suitable place.
+ */
+ GtkSpellChecker * speller = gtk_spell_checker_new();
+
+ if (! gtk_spell_checker_attach(speller, GTK_TEXT_VIEW(text_view))) {
+ g_print("gtkspell error:\n");
+ }
+#endif
+
+ gtk_widget_set_size_request (text_view, -1, 64);
+ gtk_text_view_set_editable (GTK_TEXT_VIEW (text_view), TRUE);
+ scroller.add(*Gtk::manage(Glib::wrap(text_view)));
+ text_vbox.pack_start(scroller, true, true, 0);
+
+ /* Notebook -----------------------------------*/
+ notebook.set_name( "TextEdit Notebook" );
+ notebook.append_page(font_vbox, font_label);
+ notebook.append_page(feat_vbox, feat_label);
+ notebook.append_page(text_vbox, text_label);
+
+ /* Buttons (below notebook) ------------------ */
+ setasdefault_button.set_use_underline(true);
+ apply_button.set_can_default();
+ button_row.pack_start(setasdefault_button, false, false, 0);
+ button_row.pack_end(close_button, false, false, VB_MARGIN);
+ button_row.pack_end(apply_button, false, false, VB_MARGIN);
+
+ Gtk::Box *contents = _getContents();
+ contents->set_name("TextEdit Dialog Box");
+ contents->set_spacing(4);
+ contents->pack_start(notebook, true, true);
+ contents->pack_start(button_row, false, false, VB_MARGIN);
+
+ /* Signal handlers */
+ g_signal_connect ( G_OBJECT (text_buffer), "changed", G_CALLBACK (onTextChange), this );
+ setasdefault_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onSetDefault));
+ apply_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onApply));
+ close_button.signal_clicked().connect(sigc::bind(_signal_response.make_slot(), GTK_RESPONSE_CLOSE));
+ fontChangedConn = font_selector.connectChanged (sigc::mem_fun(*this, &TextEdit::onFontChange));
+ fontFeaturesChangedConn = font_features.connectChanged(sigc::mem_fun(*this, &TextEdit::onChange));
+ notebook.signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures));
+ desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &TextEdit::setTargetDesktop) );
+ deskTrack.connect(GTK_WIDGET(gobj()));
+
+ font_selector.set_name ("TextEdit");
+
+ show_all_children();
+}
+
+TextEdit::~TextEdit()
+{
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ desktopChangeConn.disconnect();
+ deskTrack.disconnect();
+ fontChangedConn.disconnect();
+ fontFeaturesChangedConn.disconnect();
+}
+
+void TextEdit::onSelectionModified(guint flags )
+{
+ gboolean style, content;
+
+ style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 );
+
+ content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 );
+
+ onReadSelection (style, content);
+}
+
+void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ )
+{
+ if (blocked)
+ return;
+
+ if (!desktop || SP_ACTIVE_DESKTOP != desktop)
+ {
+ return;
+ }
+
+ blocked = true;
+
+ SPItem *text = getSelectedTextItem ();
+
+ Glib::ustring phrase = samplephrase;
+
+ if (text)
+ {
+ guint items = getSelectedTextCount ();
+ if (items == 1) {
+ gtk_widget_set_sensitive (text_view, TRUE);
+ } else {
+ gtk_widget_set_sensitive (text_view, FALSE);
+ }
+ apply_button.set_sensitive ( false );
+ setasdefault_button.set_sensitive ( true );
+
+ gchar *str;
+ str = sp_te_get_string_multiline (text);
+ if (str) {
+ if (items == 1) {
+ gtk_text_buffer_set_text (text_buffer, str, strlen (str));
+ gtk_text_buffer_set_modified (text_buffer, FALSE);
+ }
+ phrase = str;
+
+ } else {
+ gtk_text_buffer_set_text (text_buffer, "", 0);
+ }
+
+ text->getRepr(); // was being called but result ignored. Check this.
+ } else {
+ gtk_widget_set_sensitive (text_view, FALSE);
+ apply_button.set_sensitive ( false );
+ setasdefault_button.set_sensitive ( false );
+ }
+
+ if (dostyle) {
+
+ // create temporary style
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+
+ // Query style from desktop into it. This returns a result flag and fills query with the
+ // style of subselection, if any, or selection
+
+ int result_numbers = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+
+ // If querying returned nothing, read the style from the text tool prefs (default style for new texts).
+ if (result_numbers == QUERY_STYLE_NOTHING) {
+ query.readFromPrefs("/tools/text");
+ }
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // Update family/style based on selection.
+ font_lister->selection_update();
+ Glib::ustring fontspec = font_lister->get_fontspec();
+
+ // Update Font Face.
+ font_selector.update_font ();
+
+ // Update Size.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double size = sp_style_css_size_px_to_units(query.font_size.computed, unit);
+ font_selector.update_size (size);
+ selected_fontsize = size;
+ // Update font features (variant) widget
+ //int result_features =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS);
+ int result_features =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTFEATURESETTINGS);
+ font_features.update( &query, result_features == QUERY_STYLE_MULTIPLE_DIFFERENT, fontspec );
+ Glib::ustring features = font_features.get_markup();
+
+ // Update Preview
+ setPreviewText (fontspec, features, phrase);
+ }
+
+ blocked = false;
+}
+
+
+void TextEdit::setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase)
+{
+ if (font_spec.empty()) {
+ preview_label.set_markup("");
+ preview_label2.set_markup("");
+ return;
+ }
+
+ // Limit number of lines in preview to arbitrary amount to prevent Text and Font dialog
+ // from growing taller than a desktop
+ const int max_lines = 4;
+ // Ignore starting empty lines; they would show up as nothing
+ auto start_pos = phrase.find_first_not_of(" \n\r\t");
+ if (start_pos == Glib::ustring::npos) {
+ start_pos = 0;
+ }
+ // Now take up to max_lines
+ auto end_pos = Glib::ustring::npos;
+ auto from = start_pos;
+ for (int i = 0; i < max_lines; ++i) {
+ end_pos = phrase.find("\n", from);
+ if (end_pos == Glib::ustring::npos) { break; }
+ from = end_pos + 1;
+ }
+ Glib::ustring phrase_trimmed = phrase.substr(start_pos, end_pos != Glib::ustring::npos ? end_pos - start_pos : end_pos);
+
+ Glib::ustring font_spec_escaped = Glib::Markup::escape_text( font_spec );
+ Glib::ustring phrase_escaped = Glib::Markup::escape_text(phrase_trimmed);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double pt_size =
+ Inkscape::Util::Quantity::convert(
+ sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit), "px", "pt");
+ pt_size = std::min(pt_size, 100.0);
+
+ // Pango font size is in 1024ths of a point
+ Glib::ustring size = std::to_string( int(pt_size * PANGO_SCALE) );
+ Glib::ustring markup = "<span font=\'" + font_spec_escaped +
+ "\' size=\'" + size + "\'";
+ if (!font_features.empty()) {
+ markup += " font_features=\'" + font_features + "\'";
+ }
+ markup += ">" + phrase_escaped + "</span>";
+
+ preview_label.set_markup (markup);
+ preview_label2.set_markup (markup);
+}
+
+
+SPItem *TextEdit::getSelectedTextItem ()
+{
+ if (!SP_ACTIVE_DESKTOP)
+ return nullptr;
+
+ auto tmp= SP_ACTIVE_DESKTOP->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i))
+ return *i;
+ }
+
+ return nullptr;
+}
+
+
+unsigned TextEdit::getSelectedTextCount ()
+{
+ if (!SP_ACTIVE_DESKTOP)
+ return 0;
+
+ unsigned int items = 0;
+
+ auto tmp= SP_ACTIVE_DESKTOP->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i))
+ ++items;
+ }
+
+ return items;
+}
+
+void TextEdit::onSelectionChange()
+{
+ onReadSelection (TRUE, TRUE);
+}
+
+void TextEdit::updateObjectText ( SPItem *text )
+{
+ GtkTextIter start, end;
+
+ // write text
+ if (gtk_text_buffer_get_modified (text_buffer)) {
+ gtk_text_buffer_get_bounds (text_buffer, &start, &end);
+ gchar *str = gtk_text_buffer_get_text (text_buffer, &start, &end, TRUE);
+ sp_te_set_repr_text_multiline (text, str);
+ g_free (str);
+ gtk_text_buffer_set_modified (text_buffer, FALSE);
+ }
+}
+
+SPCSSAttr *TextEdit::fillTextStyle ()
+{
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ Glib::ustring fontspec = font_selector.get_fontspec();
+
+ if( !fontspec.empty() ) {
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->fill_css( css, fontspec );
+
+ // TODO, possibly move this to FontLister::set_css to be shared.
+ Inkscape::CSSOStringStream os;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ if (prefs->getBool("/options/font/textOutputPx", true)) {
+ os << sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit)
+ << sp_style_get_css_unit_string(SP_CSS_UNIT_PX);
+ } else {
+ os << font_selector.get_fontsize() << sp_style_get_css_unit_string(unit);
+ }
+ sp_repr_css_set_property (css, "font-size", os.str().c_str());
+ }
+
+ // Font features
+ font_features.fill_css( css );
+
+ return css;
+}
+
+void TextEdit::onSetDefault()
+{
+ SPCSSAttr *css = fillTextStyle ();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ blocked = true;
+ prefs->mergeStyle("/tools/text/style", css);
+ blocked = false;
+
+ sp_repr_css_attr_unref (css);
+
+ setasdefault_button.set_sensitive ( false );
+}
+
+void TextEdit::onApply()
+{
+ blocked = true;
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ unsigned items = 0;
+ auto item_list = desktop->getSelection()->items();
+ SPCSSAttr *css = fillTextStyle ();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ for(auto i=item_list.begin();i!=item_list.end();++i){
+ // apply style to the reprs of all text objects in the selection
+ if (SP_IS_TEXT (*i) || (SP_IS_FLOWTEXT (*i)) ) {
+ ++items;
+ }
+ }
+ if (items == 1) {
+ double factor = font_selector.get_fontsize() / selected_fontsize;
+ prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", factor);
+ }
+ sp_desktop_set_style(desktop, css, true);
+
+ if (items == 0) {
+ // no text objects; apply style to prefs for new objects
+ prefs->mergeStyle("/tools/text/style", css);
+ setasdefault_button.set_sensitive ( false );
+
+ } else if (items == 1) {
+ // exactly one text object; now set its text, too
+ SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
+ if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT(item)) {
+ updateObjectText (item);
+ SPStyle *item_style = item->style;
+ if (SP_IS_TEXT(item) && item_style->inline_size.value == 0) {
+ css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
+ sp_repr_css_unset_property(css, "inline-size");
+ item->changeCSS(css, "style");
+ }
+ }
+ }
+
+ // Update FontLister
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if( !fontspec.empty() ) {
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_fontspec( fontspec, false );
+ }
+
+ // complete the transaction
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Set text style"));
+ apply_button.set_sensitive ( false );
+
+ sp_repr_css_attr_unref (css);
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+
+ blocked = false;
+}
+
+void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos)
+{
+ if (pos == 1) {
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if (!fontspec.empty()) {
+ font_instance *res = font_factory::Default()->FaceFromFontSpecification(fontspec.c_str());
+ if (res && !res->fulloaded) {
+ res->InitTheFace(true);
+ font_features.update_opentype(fontspec);
+ }
+ }
+ }
+}
+
+void TextEdit::onChange()
+{
+ if (blocked) {
+ return;
+ }
+
+ GtkTextIter start;
+ GtkTextIter end;
+ gtk_text_buffer_get_bounds (text_buffer, &start, &end);
+ gchar *str = gtk_text_buffer_get_text(text_buffer, &start, &end, TRUE);
+
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ Glib::ustring features = font_features.get_markup();
+ const gchar *phrase = str && *str ? str : samplephrase.c_str();
+ setPreviewText(fontspec, features, phrase);
+ g_free (str);
+
+ SPItem *text = getSelectedTextItem();
+ if (text) {
+ apply_button.set_sensitive ( true );
+ }
+
+ setasdefault_button.set_sensitive ( true);
+}
+
+void TextEdit::onTextChange (GtkTextBuffer *text_buffer, TextEdit *self)
+{
+ self->onChange();
+}
+
+void TextEdit::onFontChange(Glib::ustring fontspec)
+{
+ // Is not necesary update open type features this done when user click on font features tab
+ onChange();
+}
+
+void TextEdit::setDesktop(SPDesktop *desktop)
+{
+ Panel::setDesktop(desktop);
+ deskTrack.setBase(desktop);
+}
+
+void TextEdit::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, &TextEdit::onSelectionChange)));
+ subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &TextEdit::onSelectionChange)));
+ selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &TextEdit::onSelectionModified)));
+ }
+ //widget_setup();
+ onReadSelection (TRUE, TRUE);
+ }
+}
+
+} //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/text-edit.h b/src/ui/dialog/text-edit.h
new file mode 100644
index 0000000..7a40c77
--- /dev/null
+++ b/src/ui/dialog/text-edit.h
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Text-edit
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * John Smith
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 1999-2013 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TEXT_EDIT_H
+#define INKSCAPE_UI_DIALOG_TEXT_EDIT_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/button.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/scrolledwindow.h>
+#include "ui/widget/panel.h"
+#include "ui/widget/frame.h"
+#include "ui/dialog/desktop-tracker.h"
+
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-variants.h"
+
+class SPItem;
+struct SPFontSelector;
+class font_instance;
+class SPCSSAttr;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+#define VB_MARGIN 4
+/**
+ * The TextEdit class defines the Text and font dialog.
+ *
+ * The Text and font dialog allows you to set the font family, style and size
+ * and shows a preview of the result. The dialogs layout settings include
+ * horizontal and vertical alignment and inter line distance.
+ */
+class TextEdit : public UI::Widget::Panel {
+public:
+ TextEdit();
+ ~TextEdit() override;
+
+ /**
+ * Helper function which returns a new instance of the dialog.
+ * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager).
+ */
+ static TextEdit &getInstance() { return *new TextEdit(); }
+
+protected:
+
+ /**
+ * Callback for pressing the default button.
+ */
+ void onSetDefault ();
+
+ /**
+ * Callback for pressing the apply button.
+ */
+ void onApply ();
+ void onSelectionChange ();
+ void onSelectionModified (guint flags);
+
+ /**
+ * Called whenever something 'changes' on canvas.
+ *
+ * onReadSelection gets the currently selected item from the canvas and sets all the controls in this dialog to the correct state.
+ *
+ * @param dostyle Indicates whether the modification of the user includes a style change.
+ * @param content Indicates whether the modification of the user includes a style change. Actually refers to the question if we do want to show the content? (Parameter currently not used)
+ */
+ void onReadSelection (gboolean style, gboolean content);
+
+ /**
+ * Callback invoked when the user modifies the text of the selected text object.
+ *
+ * onTextChange is responsible for initiating the commands after the user
+ * modified the text in the selected object. The UI of the dialog is
+ * updated. The subfunction setPreviewText updates the preview label.
+ *
+ * @param self pointer to the current instance of the dialog.
+ */
+ void onChange ();
+ void onFontFeatures (Gtk::Widget * widgt, int pos);
+ static void onTextChange (GtkTextBuffer *text_buffer, TextEdit *self);
+
+ /**
+ * Callback invoked when the user modifies the font through the dialog or the tools control bar.
+ *
+ * onFontChange updates the dialog UI. The subfunction setPreviewText updates the preview label.
+ *
+ * @param fontspec for the text to be previewed.
+ */
+ void onFontChange (Glib::ustring fontspec);
+
+ /**
+ * Get the selected text off the main canvas.
+ *
+ * @return SPItem pointer to the selected text object
+ */
+ SPItem *getSelectedTextItem ();
+
+ /**
+ * Count the number of text objects in the selection on the canvas.
+ */
+ unsigned getSelectedTextCount ();
+
+ /**
+ * Helper function to create markup from a fontspec and display in the preview label.
+ *
+ * @param fontspec for the text to be previewed.
+ * @param font_features for text to be previewed (in CSS format).
+ * @param phrase text to be shown.
+ */
+ void setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase);
+
+ void updateObjectText ( SPItem *text );
+ SPCSSAttr *fillTextStyle ();
+
+ /**
+ * 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.
+ *
+ * @see DesktopTracker
+ */
+ void setTargetDesktop(SPDesktop *desktop);
+
+
+
+private:
+
+ /*
+ * All the dialogs widgets
+ */
+ Gtk::Notebook notebook;
+
+ // Tab 1: Font ---------------------- //
+ Gtk::VBox font_vbox;
+ Gtk::Label font_label;
+
+ Inkscape::UI::Widget::FontSelector font_selector;
+ Inkscape::UI::Widget::FontVariations font_variations;
+ Gtk::Label preview_label; // Share with variants tab?
+
+ // Tab 2: Text ---------------------- //
+ Gtk::VBox text_vbox;
+ Gtk::Label text_label;
+
+ Gtk::ScrolledWindow scroller;
+ GtkWidget *text_view; // TODO - Convert this to a Gtk::TextView, but GtkSpell doesn't seem to work with it
+ GtkTextBuffer *text_buffer;
+
+ // Tab 3: Features ----------------- //
+ Gtk::VBox feat_vbox;
+ Inkscape::UI::Widget::FontVariants font_features;
+ Gtk::Label feat_label;
+ Gtk::Label preview_label2; // Could reparent preview_label but having a second label is probably easier.
+
+ // Shared ------- ------------------ //
+ Gtk::HBox button_row;
+ Gtk::Button setasdefault_button;
+ Gtk::Button close_button;
+ Gtk::Button apply_button;
+
+ // Signals
+ SPDesktop *desktop;
+ DesktopTracker deskTrack;
+ sigc::connection desktopChangeConn;
+ sigc::connection selectChangedConn;
+ sigc::connection subselChangedConn;
+ sigc::connection selectModifiedConn;
+ sigc::connection fontChangedConn;
+ sigc::connection fontFeaturesChangedConn;
+
+ // Other
+ double selected_fontsize;
+ bool blocked;
+ const Glib::ustring samplephrase;
+
+
+ TextEdit(TextEdit const &d) = delete;
+ TextEdit operator=(TextEdit const &d) = delete;
+};
+
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_TEXT_EDIT_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/tile.cpp b/src/ui/dialog/tile.cpp
new file mode 100644
index 0000000..ba980cb
--- /dev/null
+++ b/src/ui/dialog/tile.cpp
@@ -0,0 +1,81 @@
+// 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.
+ */
+
+#include "ui/dialog/grid-arrange-tab.h"
+#include "ui/dialog/polar-arrange-tab.h"
+
+#include <glibmm/i18n.h>
+
+#include "tile.h"
+#include "verbs.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ArrangeDialog::ArrangeDialog()
+ : UI::Widget::Panel("/dialogs/gridtiler", SP_VERB_SELECTION_ARRANGE),
+ _gridArrangeTab(new GridArrangeTab(this)),
+ _polarArrangeTab(new PolarArrangeTab(this))
+{
+ Gtk::Box *contents = this->_getContents();
+
+ _notebook.append_page(*_gridArrangeTab, C_("Arrange dialog", "Rectangular grid"));
+ _notebook.append_page(*_polarArrangeTab, C_("Arrange dialog", "Polar Coordinates"));
+ _arrangeBox.pack_start(_notebook);
+
+ _arrangeButton = this->addResponseButton(C_("Arrange dialog","_Arrange"), GTK_RESPONSE_APPLY);
+ _arrangeButton->set_use_underline(true);
+ _arrangeButton->set_tooltip_text(_("Arrange selected objects"));
+ contents->pack_start(_arrangeBox);
+ //show_all_children();
+}
+
+
+void ArrangeDialog::on_show()
+{
+ UI::Widget::Panel::on_show();
+ _polarArrangeTab->on_arrange_radio_changed();
+}
+
+void ArrangeDialog::_apply()
+{
+ switch(_notebook.get_current_page())
+ {
+ case 0:
+ _gridArrangeTab->arrange();
+ break;
+ case 1:
+ _polarArrangeTab->arrange();
+ break;
+ }
+}
+
+} //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/tile.h b/src/ui/dialog/tile.h
new file mode 100644
index 0000000..26836e1
--- /dev/null
+++ b/src/ui/dialog/tile.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for creating grid type arrangements of selected objects
+ */
+/* Authors:
+ * Bob Jamison ( based off trace dialog)
+ * John Cliff
+ * Other dudes from The Inkscape Organization
+ * 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 SEEN_UI_DIALOG_TILE_H
+#define SEEN_UI_DIALOG_TILE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/widget/panel.h"
+
+namespace Gtk {
+class Button;
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ArrangeTab;
+class GridArrangeTab;
+class PolarArrangeTab;
+
+class ArrangeDialog : public UI::Widget::Panel {
+private:
+ Gtk::VBox _arrangeBox;
+ Gtk::Notebook _notebook;
+
+ GridArrangeTab *_gridArrangeTab;
+ PolarArrangeTab *_polarArrangeTab;
+
+ Gtk::Button *_arrangeButton;
+
+public:
+ ArrangeDialog();
+ ~ArrangeDialog() override = default;;
+
+ /**
+ * Callback from Apply
+ */
+ void _apply() override;
+
+ void on_show() override;
+
+ static ArrangeDialog& getInstance() { return *new ArrangeDialog(); }
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif /* __TILEDIALOG_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/tracedialog.cpp b/src/ui/dialog/tracedialog.cpp
new file mode 100644
index 0000000..00c461d
--- /dev/null
+++ b/src/ui/dialog/tracedialog.cpp
@@ -0,0 +1,380 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bitmap tracing settings dialog - second implementation.
+ */
+/* Authors:
+ * Marc Jeanmougin <marc.jeanmougin@telecom-paristech.fr>
+ *
+ * Copyright (C) 2019 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tracedialog.h"
+
+#include <gtkmm.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/stack.h>
+
+
+#include <glibmm/i18n.h>
+
+#include "desktop-tracker.h"
+#include "desktop.h"
+#include "selection.h"
+
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "trace/autotrace/inkscape-autotrace.h"
+#include "trace/potrace/inkscape-potrace.h"
+#include "trace/depixelize/inkscape-depixelize.h"
+
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class TraceDialogImpl2 : public TraceDialog {
+ public:
+ TraceDialogImpl2();
+ ~TraceDialogImpl2() override;
+
+ private:
+ Inkscape::Trace::Tracer tracer;
+ void traceProcess(bool do_i_trace);
+ void abort();
+
+ void previewCallback();
+ bool previewResize(const Cairo::RefPtr<Cairo::Context>&);
+ void traceCallback();
+ void onSelectionModified(guint flags);
+ void onSetDefaults();
+
+ void setDesktop(SPDesktop *desktop) override;
+ void setTargetDesktop(SPDesktop *desktop);
+
+ SPDesktop *desktop;
+ DesktopTracker deskTrack;
+ sigc::connection desktopChangeConn;
+ sigc::connection selectChangedConn;
+ sigc::connection selectModifiedConn;
+
+ Glib::RefPtr<Gtk::Builder> builder;
+
+ Glib::RefPtr<Gtk::Adjustment> MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2, SS_AT_ET_T, SS_AT_FI_T, SS_BC_T, SS_CQ_T,
+ SS_ED_T, optimize, smooth, speckles;
+ Gtk::ComboBoxText *CBT_SS, *CBT_MS;
+ Gtk::CheckButton *CB_invert, *CB_MS_smooth, *CB_MS_stack, *CB_MS_rb, *CB_speckles, *CB_smooth, *CB_optimize,
+ /* *CB_live,*/ *CB_SIOX;
+ Gtk::RadioButton *RB_PA_voronoi;
+ Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update;
+ Gtk::Box *mainBox;
+ Gtk::Stack *choice_scan;
+ Gtk::Notebook *choice_tab;
+ Glib::RefPtr<Gdk::Pixbuf> scaledPreview;
+ Gtk::DrawingArea *previewArea;
+};
+
+void TraceDialogImpl2::setDesktop(SPDesktop *desktop)
+{
+ Panel::setDesktop(desktop);
+ deskTrack.setBase(desktop);
+}
+
+void TraceDialogImpl2::setTargetDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+ if (this->desktop) {
+ selectChangedConn.disconnect();
+ selectModifiedConn.disconnect();
+ }
+ this->desktop = desktop;
+ if (desktop && desktop->selection) {
+ selectModifiedConn = desktop->selection->connectModified(
+ sigc::hide<0>(sigc::mem_fun(*this, &TraceDialogImpl2::onSelectionModified)));
+ }
+ }
+}
+
+void TraceDialogImpl2::traceProcess(bool do_i_trace)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop)
+ desktop->setWaitingCursor();
+
+ if (CB_SIOX->get_active())
+ tracer.enableSiox(true);
+ else
+ tracer.enableSiox(false);
+
+ Glib::ustring type =
+ choice_scan->get_visible_child_name() == "SingleScan" ? CBT_SS->get_active_text() : CBT_MS->get_active_text();
+
+ Inkscape::Trace::Potrace::TraceType potraceType;
+ // Inkscape::Trace::Autotrace::TraceType autotraceType;
+
+ bool use_autotrace = false;
+ Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO
+
+ if (type == _("Brightness cutoff"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS;
+ else if (type == _("Edge detection"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_CANNY;
+ else if (type == _("Color quantization"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_QUANT;
+ else if (type == _("Autotrace"))
+ {
+ // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE
+ use_autotrace = true;
+ ate.opts->color_count = 2;
+ }
+ else if (type == _("Centerline tracing (autotrace)"))
+ {
+ // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE
+ use_autotrace = true;
+ ate.opts->color_count = 2;
+ ate.opts->centerline = true;
+ ate.opts->preserve_width = true;
+ }
+ else if (type == _("Brightness steps"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS_MULTI;
+ else if (type == _("Colors"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_COLOR;
+ else if (type == _("Grays"))
+ potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_MONO;
+ else if (type == _("Autotrace (slower)"))
+ {
+ // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE
+ use_autotrace = true;
+ ate.opts->color_count = (int)MS_scans->get_value() + 1;
+ }
+ else
+ {
+ g_warning("Should not happen!");
+ }
+ ate.opts->filter_iterations = (int) SS_AT_FI_T->get_value();
+ ate.opts->error_threshold = SS_AT_ET_T->get_value();
+
+ Inkscape::Trace::Potrace::PotraceTracingEngine pte(
+ potraceType, CB_invert->get_active(), (int)SS_CQ_T->get_value(), SS_BC_T->get_value(),
+ 0., // Brightness floor
+ SS_ED_T->get_value(), (int)MS_scans->get_value(), CB_MS_stack->get_active(), CB_MS_smooth->get_active(),
+ CB_MS_rb->get_active());
+ pte.potraceParams->opticurve = CB_optimize->get_active();
+ pte.potraceParams->opttolerance = optimize->get_value();
+ pte.potraceParams->alphamax = CB_smooth->get_active() ? smooth->get_value() : 0;
+ pte.potraceParams->turdsize = CB_speckles->get_active() ? (int)speckles->get_value() : 0;
+
+
+
+ //Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO
+ Inkscape::Trace::Depixelize::DepixelizeTracingEngine dte(RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::TRACE_VORONOI : Inkscape::Trace::Depixelize::TraceType::TRACE_BSPLINES, PA_curves->get_value(), (int) PA_islands->get_value(), (int) PA_sparse1->get_value(), PA_sparse2->get_value() );
+
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = tracer.getSelectedImage();
+ if (pixbuf) {
+ Glib::RefPtr<Gdk::Pixbuf> preview = use_autotrace ? ate.preview(pixbuf) : pte.preview(pixbuf);
+ if (preview) {
+ int width = preview->get_width();
+ int height = preview->get_height();
+ const Gtk::Allocation &vboxAlloc = previewArea->get_allocation();
+ double scaleFX = vboxAlloc.get_width() / (double)width;
+ double scaleFY = vboxAlloc.get_height() / (double)height;
+ double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX;
+ int newWidth = (int)(((double)width) * scaleFactor);
+ int newHeight = (int)(((double)height) * scaleFactor);
+ scaledPreview = preview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST);
+ previewArea->queue_draw();
+ }
+ }
+ if (do_i_trace){
+ if (choice_tab->get_current_page() == 1){
+ tracer.trace(&dte);
+ printf("dt\n");
+ } else if (use_autotrace) {
+ tracer.trace(&ate);
+ printf("at\n");
+ } else if (choice_tab->get_current_page() == 0) {
+ tracer.trace(&pte);
+ printf("pt\n");
+ }
+ }
+
+ if (desktop)
+ desktop->clearWaitingCursor();
+}
+
+bool TraceDialogImpl2::previewResize(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ if (!scaledPreview) return false; // return early
+ int width = scaledPreview->get_width();
+ int height = scaledPreview->get_height();
+ const Gtk::Allocation &vboxAlloc = previewArea->get_allocation();
+ double scaleFX = vboxAlloc.get_width() / (double)width;
+ double scaleFY = vboxAlloc.get_height() / (double)height;
+ double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX;
+ int newWidth = (int)(((double)width) * scaleFactor);
+ int newHeight = (int)(((double)height) * scaleFactor);
+ int offsetX = (vboxAlloc.get_width() - newWidth)/2;
+ int offsetY = (vboxAlloc.get_height() - newHeight)/2;
+
+ Glib::RefPtr<Gdk::Pixbuf> temp = scaledPreview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST);
+ Gdk::Cairo::set_source_pixbuf(cr, temp, offsetX, offsetY);
+ cr->paint();
+ return false;
+}
+
+void TraceDialogImpl2::abort()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop)
+ desktop->clearWaitingCursor();
+ tracer.abort();
+}
+
+void TraceDialogImpl2::onSelectionModified(guint flags)
+{
+ if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ previewCallback();
+ }
+}
+
+void TraceDialogImpl2::onSetDefaults()
+{
+ MS_scans->set_value(8);
+ PA_curves->set_value(1);
+ PA_islands->set_value(5);
+ PA_sparse1->set_value(4);
+ PA_sparse2->set_value(1);
+ SS_AT_FI_T->set_value(4);
+ SS_AT_ET_T->set_value(2);
+ SS_BC_T->set_value(0.45);
+ SS_CQ_T->set_value(64);
+ SS_ED_T->set_value(.65);
+ optimize->set_value(0.2);
+ smooth->set_value(1);
+ speckles->set_value(2);
+ CB_invert->set_active(false);
+ CB_MS_smooth->set_active(true);
+ CB_MS_stack->set_active(true);
+ CB_MS_rb->set_active(false);
+ CB_speckles->set_active(true);
+ CB_smooth->set_active(true);
+ CB_optimize->set_active(true);
+ //CB_live->set_active(false);
+ CB_SIOX->set_active(false);
+}
+
+void TraceDialogImpl2::previewCallback() { traceProcess(false); }
+void TraceDialogImpl2::traceCallback() { traceProcess(true); }
+
+
+TraceDialogImpl2::TraceDialogImpl2()
+ : TraceDialog()
+{
+ const std::string req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2",
+ "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T",
+ "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth",
+ "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize",
+ /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS", "B_RESET",
+ "B_STOP", "B_OK", "mainBox", "choice_tab", "choice_scan",
+ "previewArea" };
+ Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-trace.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;
+ }
+
+ Glib::RefPtr<Glib::Object> test;
+ for (std::string w : req_widgets) {
+ test = builder->get_object(w);
+ if (!test) {
+ g_warning("Required widget %s does not exist", w.c_str());
+ return;
+ }
+ }
+
+#define GET_O(name) \
+ tmp = builder->get_object(#name); \
+ name = Glib::RefPtr<Gtk::Adjustment>::cast_dynamic(tmp);
+
+ Glib::RefPtr<Glib::Object> tmp;
+
+#define GET_W(name) builder->get_widget(#name, name);
+ GET_O(MS_scans)
+ GET_O(PA_curves)
+ GET_O(PA_islands)
+ GET_O(PA_sparse1)
+ GET_O(PA_sparse2)
+ GET_O(SS_AT_FI_T)
+ GET_O(SS_AT_ET_T)
+ GET_O(SS_BC_T)
+ GET_O(SS_CQ_T)
+ GET_O(SS_ED_T)
+ GET_O(optimize)
+ GET_O(smooth)
+ GET_O(speckles)
+
+ GET_W(CB_invert)
+ GET_W(CB_MS_smooth)
+ GET_W(CB_MS_stack)
+ GET_W(CB_MS_rb)
+ GET_W(CB_speckles)
+ GET_W(CB_smooth)
+ GET_W(CB_optimize)
+ //GET_W(CB_live)
+ GET_W(CB_SIOX)
+ GET_W(RB_PA_voronoi)
+ GET_W(CBT_SS)
+ GET_W(CBT_MS)
+ GET_W(B_RESET)
+ GET_W(B_STOP)
+ GET_W(B_OK)
+ GET_W(B_Update)
+ GET_W(mainBox)
+ GET_W(choice_tab)
+ GET_W(choice_scan)
+ GET_W(previewArea)
+#undef GET_W
+#undef GET_O
+ _getContents()->add(*mainBox);
+ // show_all_children();
+ desktopChangeConn = deskTrack.connectDesktopChanged(sigc::mem_fun(*this, &TraceDialogImpl2::setTargetDesktop));
+ deskTrack.connect(GTK_WIDGET(gobj()));
+
+ B_Update->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewCallback));
+ B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::traceCallback));
+ B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::abort));
+ B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::onSetDefaults));
+ previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewResize));
+}
+
+
+TraceDialogImpl2::~TraceDialogImpl2()
+{
+ selectChangedConn.disconnect();
+ selectModifiedConn.disconnect();
+ desktopChangeConn.disconnect();
+}
+
+
+
+TraceDialog &TraceDialog::getInstance()
+{
+ TraceDialog *dialog = new TraceDialogImpl2();
+ return *dialog;
+}
+
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/tracedialog.h b/src/ui/dialog/tracedialog.h
new file mode 100644
index 0000000..d02e705
--- /dev/null
+++ b/src/ui/dialog/tracedialog.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Bitmap tracing settings dialog
+ */
+/* 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 __TRACEDIALOG_H__
+#define __TRACEDIALOG_H__
+
+#include "ui/widget/panel.h"
+#include "verbs.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * A dialog that displays log messages
+ */
+class TraceDialog : public UI::Widget::Panel
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ TraceDialog() :
+ UI::Widget::Panel("/dialogs/trace", SP_VERB_SELECTION_TRACE)
+ {}
+
+
+ /**
+ * Factory method
+ */
+ static TraceDialog &getInstance();
+
+ /**
+ * Destructor
+ */
+ ~TraceDialog() override = default;;
+
+
+};
+
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* __TRACEDIALOG_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/transformation.cpp b/src/ui/dialog/transformation.cpp
new file mode 100644
index 0000000..f212654
--- /dev/null
+++ b/src/ui/dialog/transformation.cpp
@@ -0,0 +1,1171 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Transform dialog - implementation.
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * buliabyak@gmail.com
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/dialog.h>
+
+#include <2geom/transforms.h>
+
+#include "align-and-distribute.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "transformation.h"
+#include "verbs.h"
+
+#include "object/sp-item-transform.h"
+#include "object/sp-namedview.h"
+#include "ui/icon-loader.h"
+
+#include "ui/icon-names.h"
+
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static void on_selection_changed(Inkscape::Selection *selection, Transformation *daad)
+{
+ int page = daad->getCurrentPage();
+ daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection);
+}
+
+static void on_selection_modified(Inkscape::Selection *selection, Transformation *daad)
+{
+ int page = daad->getCurrentPage();
+ daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection);
+}
+
+/*########################################################################
+# C O N S T R U C T O R
+########################################################################*/
+
+Transformation::Transformation()
+ : UI::Widget::Panel("/dialogs/transformation", SP_VERB_DIALOG_TRANSFORM),
+ _page_move (4, 2),
+ _page_scale (4, 2),
+ _page_rotate (4, 2),
+ _page_skew (4, 2),
+ _page_transform (3, 3),
+ _scalar_move_horizontal (_("_Horizontal:"), _("Horizontal displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
+ "", "transform-move-horizontal", &_units_move),
+ _scalar_move_vertical (_("_Vertical:"), _("Vertical displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
+ "", "transform-move-vertical", &_units_move),
+ _scalar_scale_horizontal(_("_Width:"), _("Horizontal size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
+ "", "transform-scale-horizontal", &_units_scale),
+ _scalar_scale_vertical (_("_Height:"), _("Vertical size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
+ "", "transform-scale-vertical", &_units_scale),
+ _scalar_rotate (_("A_ngle:"), _("Rotation angle (positive = counterclockwise)"), UNIT_TYPE_RADIAL,
+ "", "transform-rotate", &_units_rotate),
+ _scalar_skew_horizontal (_("_Horizontal:"), _("Horizontal skew angle (positive = counterclockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
+ "", "transform-skew-horizontal", &_units_skew),
+ _scalar_skew_vertical (_("_Vertical:"), _("Vertical skew angle (positive = clockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
+ "", "transform-skew-vertical", &_units_skew),
+
+ _scalar_transform_a ("_A:", _("Transformation matrix element A")),
+ _scalar_transform_b ("_B:", _("Transformation matrix element B")),
+ _scalar_transform_c ("_C:", _("Transformation matrix element C")),
+ _scalar_transform_d ("_D:", _("Transformation matrix element D")),
+ _scalar_transform_e ("_E:", _("Transformation matrix element E")),
+ _scalar_transform_f ("_F:", _("Transformation matrix element F")),
+
+ _counterclockwise_rotate (),
+ _clockwise_rotate (),
+
+ _check_move_relative (_("Rela_tive move")),
+ _check_scale_proportional (_("_Scale proportionally")),
+ _check_apply_separately (_("Apply to each _object separately")),
+ _check_replace_matrix (_("Edit c_urrent matrix"))
+
+{
+ _check_move_relative.set_use_underline();
+ _check_move_relative.set_tooltip_text(_("Add the specified relative displacement to the current position; otherwise, edit the current absolute position directly"));
+ _check_scale_proportional.set_use_underline();
+ _check_scale_proportional.set_tooltip_text(_("Preserve the width/height ratio of the scaled objects"));
+ _check_apply_separately.set_use_underline();
+ _check_apply_separately.set_tooltip_text(_("Apply the scale/rotate/skew to each selected object separately; otherwise, transform the selection as a whole"));
+ _check_replace_matrix.set_use_underline();
+ _check_replace_matrix.set_tooltip_text(_("Edit the current transform= matrix; otherwise, post-multiply transform= by this matrix"));
+ Gtk::Box *contents = _getContents();
+
+ contents->set_spacing(0);
+
+ // Notebook for individual transformations
+ contents->pack_start(_notebook, false, false);
+
+ _page_move.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_move, _("_Move"), true);
+ layoutPageMove();
+
+ _page_scale.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_scale, _("_Scale"), true);
+ layoutPageScale();
+
+ _page_rotate.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_rotate, _("_Rotate"), true);
+ layoutPageRotate();
+
+ _page_skew.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_skew, _("Ske_w"), true);
+ layoutPageSkew();
+
+ _page_transform.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_transform, _("Matri_x"), true);
+ layoutPageTransform();
+
+ _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage));
+
+ // Apply separately
+ contents->pack_start(_check_apply_separately, false, false);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _check_apply_separately.set_active(prefs->getBool("/dialogs/transformation/applyseparately"));
+ _check_apply_separately.signal_toggled().connect(sigc::mem_fun(*this, &Transformation::onApplySeparatelyToggled));
+
+ // make sure all spinbuttons activate Apply on pressing Enter
+ ((Gtk::Entry *) (_scalar_move_horizontal.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_rotate.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->set_activates_default(true);
+ ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->set_activates_default(true);
+
+ updateSelection(PAGE_MOVE, _getSelection());
+
+ resetButton = addResponseButton(_("_Clear"), 0);
+ if (resetButton) {
+ resetButton->set_tooltip_text(_("Reset the values on the current tab to defaults"));
+ resetButton->set_sensitive(true);
+ resetButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onClear));
+ }
+
+ applyButton = addResponseButton(_("_Apply"), Gtk::RESPONSE_APPLY);
+ if (applyButton) {
+ applyButton->set_tooltip_text(_("Apply transformation to selection"));
+ applyButton->set_sensitive(false);
+ }
+
+ // Connect to the global selection changed & modified signals
+ _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::bind(sigc::ptr_fun(&on_selection_changed), this));
+ _selModifyConn = INKSCAPE.signal_selection_modified.connect(sigc::hide<1>(sigc::bind(sigc::ptr_fun(&on_selection_modified), this)));
+
+ _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Transformation::setDesktop) );
+ _deskTrack.connect(GTK_WIDGET(gobj()));
+
+ show_all_children();
+}
+
+Transformation::~Transformation()
+{
+ _selModifyConn.disconnect();
+ _selChangeConn.disconnect();
+ _desktopChangeConn.disconnect();
+ _deskTrack.disconnect();
+}
+
+void Transformation::setTargetDesktop(SPDesktop *desktop)
+{
+ if (_desktop != desktop) {
+ _desktop = desktop;
+ }
+}
+
+/*########################################################################
+# U T I L I T Y
+########################################################################*/
+
+void Transformation::presentPage(Transformation::PageType page)
+{
+ _notebook.set_current_page(page);
+ show();
+ present();
+}
+
+
+
+
+/*########################################################################
+# S E T U P L A Y O U T
+########################################################################*/
+
+
+void Transformation::layoutPageMove()
+{
+ _units_move.setUnitType(UNIT_TYPE_LINEAR);
+
+ // Setting default unit to document unit
+ SPDesktop *dt = getDesktop();
+ SPNamedView *nv = dt->getNamedView();
+ if (nv->display_units) {
+ _units_move.setUnit(nv->display_units->abbr);
+ }
+
+ _scalar_move_horizontal.initScalar(-1e6, 1e6);
+ _scalar_move_horizontal.setDigits(3);
+ _scalar_move_horizontal.setIncrements(0.1, 1.0);
+ _scalar_move_horizontal.set_hexpand();
+
+ _scalar_move_vertical.initScalar(-1e6, 1e6);
+ _scalar_move_vertical.setDigits(3);
+ _scalar_move_vertical.setIncrements(0.1, 1.0);
+ _scalar_move_vertical.set_hexpand();
+
+ //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_HOR );
+
+ _page_move.table().attach(_scalar_move_horizontal, 0, 0, 2, 1);
+ _page_move.table().attach(_units_move, 2, 0, 1, 1);
+
+ _scalar_move_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
+
+ //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_VER );
+ _page_move.table().attach(_scalar_move_vertical, 0, 1, 2, 1);
+
+ _scalar_move_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
+
+ // Relative moves
+ _page_move.table().attach(_check_move_relative, 0, 2, 2, 1);
+
+ _check_move_relative.set_active(true);
+ _check_move_relative.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveRelativeToggled));
+}
+
+void Transformation::layoutPageScale()
+{
+ _units_scale.setUnitType(UNIT_TYPE_DIMENSIONLESS);
+ _units_scale.setUnitType(UNIT_TYPE_LINEAR);
+
+ _scalar_scale_horizontal.initScalar(-1e6, 1e6);
+ _scalar_scale_horizontal.setValue(100.0, "%");
+ _scalar_scale_horizontal.setDigits(3);
+ _scalar_scale_horizontal.setIncrements(0.1, 1.0);
+ _scalar_scale_horizontal.setAbsoluteIsIncrement(true);
+ _scalar_scale_horizontal.setPercentageIsIncrement(true);
+ _scalar_scale_horizontal.set_hexpand();
+
+ _scalar_scale_vertical.initScalar(-1e6, 1e6);
+ _scalar_scale_vertical.setValue(100.0, "%");
+ _scalar_scale_vertical.setDigits(3);
+ _scalar_scale_vertical.setIncrements(0.1, 1.0);
+ _scalar_scale_vertical.setAbsoluteIsIncrement(true);
+ _scalar_scale_vertical.setPercentageIsIncrement(true);
+ _scalar_scale_vertical.set_hexpand();
+
+ _page_scale.table().attach(_scalar_scale_horizontal, 0, 0, 2, 1);
+
+ _scalar_scale_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleXValueChanged));
+
+ _page_scale.table().attach(_units_scale, 2, 0, 1, 1);
+ _page_scale.table().attach(_scalar_scale_vertical, 0, 1, 2, 1);
+
+ _scalar_scale_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleYValueChanged));
+
+ _page_scale.table().attach(_check_scale_proportional, 0, 2, 2, 1);
+
+ _check_scale_proportional.set_active(false);
+ _check_scale_proportional.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleProportionalToggled));
+
+ //TODO: add a widget for selecting the fixed point in scaling, or honour rotation center?
+}
+
+void Transformation::layoutPageRotate()
+{
+ _units_rotate.setUnitType(UNIT_TYPE_RADIAL);
+
+ _scalar_rotate.initScalar(-360.0, 360.0);
+ _scalar_rotate.setDigits(3);
+ _scalar_rotate.setIncrements(0.1, 1.0);
+ _scalar_rotate.set_hexpand();
+
+ auto object_rotate_left_icon = Gtk::manage(sp_get_icon_image("object-rotate-left", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ _counterclockwise_rotate.add(*object_rotate_left_icon);
+ _counterclockwise_rotate.set_mode(false);
+ _counterclockwise_rotate.set_relief(Gtk::RELIEF_NONE);
+ _counterclockwise_rotate.set_tooltip_text(_("Rotate in a counterclockwise direction"));
+
+ auto object_rotate_right_icon = Gtk::manage(sp_get_icon_image("object-rotate-right", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ _clockwise_rotate.add(*object_rotate_right_icon);
+ _clockwise_rotate.set_mode(false);
+ _clockwise_rotate.set_relief(Gtk::RELIEF_NONE);
+ _clockwise_rotate.set_tooltip_text(_("Rotate in a clockwise direction"));
+
+ Gtk::RadioButton::Group group = _counterclockwise_rotate.get_group();
+ _clockwise_rotate.set_group(group);
+
+ _page_rotate.table().attach(_scalar_rotate, 0, 0, 2, 1);
+ _page_rotate.table().attach(_units_rotate, 2, 0, 1, 1);
+ _page_rotate.table().attach(_counterclockwise_rotate, 3, 0, 1, 1);
+ _page_rotate.table().attach(_clockwise_rotate, 4, 0, 1, 1);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE) != getDesktop()->is_yaxisdown()) {
+ _counterclockwise_rotate.set_active();
+ onRotateCounterclockwiseClicked();
+ } else {
+ _clockwise_rotate.set_active();
+ onRotateClockwiseClicked();
+ }
+
+ _scalar_rotate.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onRotateValueChanged));
+
+ _counterclockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateCounterclockwiseClicked));
+ _clockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateClockwiseClicked));
+
+ //TODO: honour rotation center?
+}
+
+void Transformation::layoutPageSkew()
+{
+ _units_skew.setUnitType(UNIT_TYPE_LINEAR);
+ _units_skew.setUnitType(UNIT_TYPE_DIMENSIONLESS);
+ _units_skew.setUnitType(UNIT_TYPE_RADIAL);
+
+ _scalar_skew_horizontal.initScalar(-1e6, 1e6);
+ _scalar_skew_horizontal.setDigits(3);
+ _scalar_skew_horizontal.setIncrements(0.1, 1.0);
+ _scalar_skew_horizontal.set_hexpand();
+
+ _scalar_skew_vertical.initScalar(-1e6, 1e6);
+ _scalar_skew_vertical.setDigits(3);
+ _scalar_skew_vertical.setIncrements(0.1, 1.0);
+ _scalar_skew_vertical.set_hexpand();
+
+ _page_skew.table().attach(_scalar_skew_horizontal, 0, 0, 2, 1);
+ _page_skew.table().attach(_units_skew, 2, 0, 1, 1);
+ _page_skew.table().attach(_scalar_skew_vertical, 0, 1, 2, 1);
+
+ _scalar_skew_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
+ _scalar_skew_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
+
+ //TODO: honour rotation center?
+}
+
+
+
+void Transformation::layoutPageTransform()
+{
+ _scalar_transform_a.setWidgetSizeRequest(65, -1);
+ _scalar_transform_a.setRange(-1e10, 1e10);
+ _scalar_transform_a.setDigits(3);
+ _scalar_transform_a.setIncrements(0.1, 1.0);
+ _scalar_transform_a.setValue(1.0);
+ _scalar_transform_a.setWidthChars(6);
+ _scalar_transform_a.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_a, 0, 0, 1, 1);
+
+ _scalar_transform_a.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ _scalar_transform_b.setWidgetSizeRequest(65, -1);
+ _scalar_transform_b.setRange(-1e10, 1e10);
+ _scalar_transform_b.setDigits(3);
+ _scalar_transform_b.setIncrements(0.1, 1.0);
+ _scalar_transform_b.setValue(0.0);
+ _scalar_transform_b.setWidthChars(6);
+ _scalar_transform_b.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_b, 0, 1, 1, 1);
+
+ _scalar_transform_b.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ _scalar_transform_c.setWidgetSizeRequest(65, -1);
+ _scalar_transform_c.setRange(-1e10, 1e10);
+ _scalar_transform_c.setDigits(3);
+ _scalar_transform_c.setIncrements(0.1, 1.0);
+ _scalar_transform_c.setValue(0.0);
+ _scalar_transform_c.setWidthChars(6);
+ _scalar_transform_c.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_c, 1, 0, 1, 1);
+
+ _scalar_transform_c.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_d.setWidgetSizeRequest(65, -1);
+ _scalar_transform_d.setRange(-1e10, 1e10);
+ _scalar_transform_d.setDigits(3);
+ _scalar_transform_d.setIncrements(0.1, 1.0);
+ _scalar_transform_d.setValue(1.0);
+ _scalar_transform_d.setWidthChars(6);
+ _scalar_transform_d.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_d, 1, 1, 1, 1);
+
+ _scalar_transform_d.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_e.setWidgetSizeRequest(65, -1);
+ _scalar_transform_e.setRange(-1e10, 1e10);
+ _scalar_transform_e.setDigits(3);
+ _scalar_transform_e.setIncrements(0.1, 1.0);
+ _scalar_transform_e.setValue(0.0);
+ _scalar_transform_e.setWidthChars(6);
+ _scalar_transform_e.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_e, 2, 0, 1, 1);
+
+ _scalar_transform_e.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_f.setWidgetSizeRequest(65, -1);
+ _scalar_transform_f.setRange(-1e10, 1e10);
+ _scalar_transform_f.setDigits(3);
+ _scalar_transform_f.setIncrements(0.1, 1.0);
+ _scalar_transform_f.setValue(0.0);
+ _scalar_transform_f.setWidthChars(6);
+ _scalar_transform_f.set_hexpand();
+
+ _page_transform.table().attach(_scalar_transform_f, 2, 1, 1, 1);
+
+ _scalar_transform_f.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ // Edit existing matrix
+ _page_transform.table().attach(_check_replace_matrix, 0, 2, 2, 1);
+
+ _check_replace_matrix.set_active(false);
+ _check_replace_matrix.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onReplaceMatrixToggled));
+}
+
+
+/*########################################################################
+# U P D A T E
+########################################################################*/
+
+void Transformation::updateSelection(PageType page, Inkscape::Selection *selection)
+{
+ if (!selection || selection->isEmpty())
+ return;
+
+ switch (page) {
+ case PAGE_MOVE: {
+ updatePageMove(selection);
+ break;
+ }
+ case PAGE_SCALE: {
+ updatePageScale(selection);
+ break;
+ }
+ case PAGE_ROTATE: {
+ updatePageRotate(selection);
+ break;
+ }
+ case PAGE_SKEW: {
+ updatePageSkew(selection);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ updatePageTransform(selection);
+ break;
+ }
+ case PAGE_QTY: {
+ break;
+ }
+ }
+
+ setResponseSensitive(Gtk::RESPONSE_APPLY,
+ selection && !selection->isEmpty());
+}
+
+void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
+{
+ updateSelection((PageType)pagenum, getDesktop()->getSelection());
+}
+
+
+void Transformation::updatePageMove(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ if (!_check_move_relative.get_active()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double x = bbox->min()[Geom::X];
+ double y = bbox->min()[Geom::Y];
+
+ double conversion = _units_move.getConversion("px");
+ _scalar_move_horizontal.setValue(x / conversion);
+ _scalar_move_vertical.setValue(y / conversion);
+ }
+ } else {
+ // do nothing, so you can apply the same relative move to many objects in turn
+ }
+ _page_move.set_sensitive(true);
+ } else {
+ _page_move.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageScale(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double w = bbox->dimensions()[Geom::X];
+ double h = bbox->dimensions()[Geom::Y];
+ _scalar_scale_horizontal.setHundredPercent(w);
+ _scalar_scale_vertical.setHundredPercent(h);
+ onScaleXValueChanged(); // to update x/y proportionality if switch is on
+ _page_scale.set_sensitive(true);
+ } else {
+ _page_scale.set_sensitive(false);
+ }
+ } else {
+ _page_scale.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageRotate(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ _page_rotate.set_sensitive(true);
+ } else {
+ _page_rotate.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageSkew(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double w = bbox->dimensions()[Geom::X];
+ double h = bbox->dimensions()[Geom::Y];
+ _scalar_skew_vertical.setHundredPercent(w);
+ _scalar_skew_horizontal.setHundredPercent(h);
+ _page_skew.set_sensitive(true);
+ } else {
+ _page_skew.set_sensitive(false);
+ }
+ } else {
+ _page_skew.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageTransform(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ if (_check_replace_matrix.get_active()) {
+ Geom::Affine current (selection->items().front()->transform); // take from the first item in selection
+
+ Geom::Affine new_displayed = current;
+
+ _scalar_transform_a.setValue(new_displayed[0]);
+ _scalar_transform_b.setValue(new_displayed[1]);
+ _scalar_transform_c.setValue(new_displayed[2]);
+ _scalar_transform_d.setValue(new_displayed[3]);
+ _scalar_transform_e.setValue(new_displayed[4]);
+ _scalar_transform_f.setValue(new_displayed[5]);
+ } else {
+ // do nothing, so you can apply the same matrix to many objects in turn
+ }
+ _page_transform.set_sensitive(true);
+ } else {
+ _page_transform.set_sensitive(false);
+ }
+}
+
+
+
+
+
+/*########################################################################
+# A P P L Y
+########################################################################*/
+
+
+
+void Transformation::_apply()
+{
+ Inkscape::Selection * const selection = _getSelection();
+ if (!selection || selection->isEmpty())
+ return;
+
+ int const page = _notebook.get_current_page();
+
+ switch (page) {
+ case PAGE_MOVE: {
+ applyPageMove(selection);
+ break;
+ }
+ case PAGE_ROTATE: {
+ applyPageRotate(selection);
+ break;
+ }
+ case PAGE_SCALE: {
+ applyPageScale(selection);
+ break;
+ }
+ case PAGE_SKEW: {
+ applyPageSkew(selection);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ applyPageTransform(selection);
+ break;
+ }
+ }
+
+ //Let's play with never turning this off
+ //setResponseSensitive(Gtk::RESPONSE_APPLY, false);
+}
+
+void Transformation::applyPageMove(Inkscape::Selection *selection)
+{
+ double x = _scalar_move_horizontal.getValue("px");
+ double y = _scalar_move_vertical.getValue("px");
+ if (_check_move_relative.get_active()) {
+ y *= getDesktop()->yaxisdir();
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/dialogs/transformation/applyseparately")) {
+ // move selection as a whole
+ if (_check_move_relative.get_active()) {
+ selection->moveRelative(x, y);
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
+ }
+ }
+ } else {
+
+ if (_check_move_relative.get_active()) {
+ // shift each object relatively to the previous one
+ std::vector<SPItem*> selected(selection->items().begin(), selection->items().end());
+ if (selected.empty()) return;
+
+ if (fabs(x) > 1e-6) {
+ std::vector< BBoxSort > sorted;
+ for (auto item : selected)
+ {
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ sorted.emplace_back(item, *bbox, Geom::X, x > 0? 1. : 0., x > 0? 0. : 1.);
+ }
+ }
+ //sort bbox by anchors
+ std::stable_sort(sorted.begin(), sorted.end());
+
+ double move = x;
+ for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
+ it < sorted.end();
+ ++it )
+ {
+ it->item->move_rel(Geom::Translate(move, 0));
+ // move each next object by x relative to previous
+ move += x;
+ }
+ }
+ if (fabs(y) > 1e-6) {
+ std::vector< BBoxSort > sorted;
+ for (auto item : selected)
+ {
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ sorted.emplace_back(item, *bbox, Geom::Y, y > 0? 1. : 0., y > 0? 0. : 1.);
+ }
+ }
+ //sort bbox by anchors
+ std::stable_sort(sorted.begin(), sorted.end());
+
+ double move = y;
+ for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
+ it < sorted.end();
+ ++it )
+ {
+ it->item->move_rel(Geom::Translate(0, move));
+ // move each next object by x relative to previous
+ move += y;
+ }
+ }
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
+ }
+ }
+ }
+
+ DocumentUndo::done( selection->desktop()->getDocument() , SP_VERB_DIALOG_TRANSFORM,
+ _("Move"));
+}
+
+void Transformation::applyPageScale(Inkscape::Selection *selection)
+{
+ double scaleX = _scalar_scale_horizontal.getValue("px");
+ double scaleY = _scalar_scale_vertical.getValue("px");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
+ bool preserve = prefs->getBool("/options/preservetransform/value", false);
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto tmp= selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ Geom::OptRect bbox_pref = item->desktopPreferredBounds();
+ Geom::OptRect bbox_geom = item->desktopGeometricBounds();
+ if (bbox_pref && bbox_geom) {
+ double new_width = scaleX;
+ double new_height = scaleY;
+ // the values are increments!
+ if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
+ new_width = scaleX/100 * bbox_pref->width();
+ new_height = scaleY/100 * bbox_pref->height();
+ }
+ if (fabs(new_width) < 1e-6) new_width = 1e-6; // not 0, as this would result in a nasty no-bbox object
+ if (fabs(new_height) < 1e-6) new_height = 1e-6;
+
+ double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
+ double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
+ double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
+ double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
+
+ Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
+ item->set_i2d_affine(item->i2dt_affine() * scaler);
+ item->doWriteTransform(item->transform);
+ }
+ }
+ } else {
+ Geom::OptRect bbox_pref = selection->preferredBounds();
+ Geom::OptRect bbox_geom = selection->geometricBounds();
+ if (bbox_pref && bbox_geom) {
+ // the values are increments!
+ double new_width = scaleX;
+ double new_height = scaleY;
+ if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
+ new_width = scaleX/100 * bbox_pref->width();
+ new_height = scaleY/100 * bbox_pref->height();
+ }
+ if (fabs(new_width) < 1e-6) new_width = 1e-6;
+ if (fabs(new_height) < 1e-6) new_height = 1e-6;
+
+ double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
+ double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
+ double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
+ double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
+ Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
+
+ selection->applyAffine(scaler);
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM,
+ _("Scale"));
+}
+
+void Transformation::applyPageRotate(Inkscape::Selection *selection)
+{
+ double angle = _scalar_rotate.getValue(DEG);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE)) {
+ angle *= -1;
+ }
+
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto tmp= selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ item->rotate_rel(Geom::Rotate (angle*M_PI/180.0));
+ }
+ } else {
+ boost::optional<Geom::Point> center = selection->center();
+ if (center) {
+ selection->rotateRelative(*center, angle);
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM,
+ _("Rotate"));
+}
+
+void Transformation::applyPageSkew(Inkscape::Selection *selection)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto items = selection->items();
+ for(auto i = items.begin();i!=items.end();++i){
+ SPItem *item = *i;
+
+ if (!_units_skew.isAbsolute()) { // percentage
+ double skewX = _scalar_skew_horizontal.getValue("%");
+ double skewY = _scalar_skew_vertical.getValue("%");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ item->skew_rel(0.01*skewX, 0.01*skewY);
+ } else if (_units_skew.isRadial()) { //deg or rad
+ double angleX = _scalar_skew_horizontal.getValue("rad");
+ double angleY = _scalar_skew_vertical.getValue("rad");
+ if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
+ || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ double skewX = tan(angleX);
+ double skewY = tan(angleY);
+ skewX *= getDesktop()->yaxisdir();
+ skewY *= getDesktop()->yaxisdir();
+ item->skew_rel(skewX, skewY);
+ } else { // absolute displacement
+ double skewX = _scalar_skew_horizontal.getValue("px");
+ double skewY = _scalar_skew_vertical.getValue("px");
+ skewY *= getDesktop()->yaxisdir();
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ double width = bbox->dimensions()[Geom::X];
+ double height = bbox->dimensions()[Geom::Y];
+ if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ item->skew_rel(skewX/height, skewY/width);
+ }
+ }
+ }
+ } else { // transform whole selection
+ Geom::OptRect bbox = selection->preferredBounds();
+ boost::optional<Geom::Point> center = selection->center();
+
+ if ( bbox && center ) {
+ double width = bbox->dimensions()[Geom::X];
+ double height = bbox->dimensions()[Geom::Y];
+
+ if (!_units_skew.isAbsolute()) { // percentage
+ double skewX = _scalar_skew_horizontal.getValue("%");
+ double skewY = _scalar_skew_vertical.getValue("%");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ selection->skewRelative(*center, 0.01 * skewX, 0.01 * skewY);
+ } else if (_units_skew.isRadial()) { //deg or rad
+ double angleX = _scalar_skew_horizontal.getValue("rad");
+ double angleY = _scalar_skew_vertical.getValue("rad");
+ if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
+ || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ double skewX = tan(angleX);
+ double skewY = tan(angleY);
+ skewX *= getDesktop()->yaxisdir();
+ skewY *= getDesktop()->yaxisdir();
+ selection->skewRelative(*center, skewX, skewY);
+ } else { // absolute displacement
+ double skewX = _scalar_skew_horizontal.getValue("px");
+ double skewY = _scalar_skew_vertical.getValue("px");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ selection->skewRelative(*center, skewX / height, skewY / width);
+ }
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM,
+ _("Skew"));
+}
+
+
+void Transformation::applyPageTransform(Inkscape::Selection *selection)
+{
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue();
+ double f = _scalar_transform_f.getValue();
+
+ Geom::Affine displayed(a, b, c, d, e, f);
+ if (displayed.isSingular()) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+
+ if (_check_replace_matrix.get_active()) {
+ auto tmp = selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ item->set_item_transform(displayed);
+ item->updateRepr();
+ }
+ } else {
+ selection->applyAffine(displayed); // post-multiply each object's transform
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM,
+ _("Edit transformation matrix"));
+}
+
+
+
+
+
+/*########################################################################
+# V A L U E - C H A N G E D C A L L B A C K S
+########################################################################*/
+
+void Transformation::onMoveValueChanged()
+{
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+}
+
+void Transformation::onMoveRelativeToggled()
+{
+ Inkscape::Selection *selection = _getSelection();
+
+ if (!selection || selection->isEmpty())
+ return;
+
+ double x = _scalar_move_horizontal.getValue("px");
+ double y = _scalar_move_vertical.getValue("px");
+
+ double conversion = _units_move.getConversion("px");
+
+ //g_message("onMoveRelativeToggled: %f, %f px\n", x, y);
+
+ Geom::OptRect bbox = selection->preferredBounds();
+
+ if (bbox) {
+ if (_check_move_relative.get_active()) {
+ // From absolute to relative
+ _scalar_move_horizontal.setValue((x - bbox->min()[Geom::X]) / conversion);
+ _scalar_move_vertical.setValue(( y - bbox->min()[Geom::Y]) / conversion);
+ } else {
+ // From relative to absolute
+ _scalar_move_horizontal.setValue((bbox->min()[Geom::X] + x) / conversion);
+ _scalar_move_vertical.setValue(( bbox->min()[Geom::Y] + y) / conversion);
+ }
+ }
+
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+}
+
+void Transformation::onScaleXValueChanged()
+{
+ if (_scalar_scale_horizontal.setProgrammatically) {
+ _scalar_scale_horizontal.setProgrammatically = false;
+ return;
+ }
+
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+
+ if (_check_scale_proportional.get_active()) {
+ if (!_units_scale.isAbsolute()) { // percentage, just copy over
+ _scalar_scale_vertical.setValue(_scalar_scale_horizontal.getValue("%"));
+ } else {
+ double scaleXPercentage = _scalar_scale_horizontal.getAsPercentage();
+ _scalar_scale_vertical.setFromPercentage (scaleXPercentage);
+ }
+ }
+}
+
+void Transformation::onScaleYValueChanged()
+{
+ if (_scalar_scale_vertical.setProgrammatically) {
+ _scalar_scale_vertical.setProgrammatically = false;
+ return;
+ }
+
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+
+ if (_check_scale_proportional.get_active()) {
+ if (!_units_scale.isAbsolute()) { // percentage, just copy over
+ _scalar_scale_horizontal.setValue(_scalar_scale_vertical.getValue("%"));
+ } else {
+ double scaleYPercentage = _scalar_scale_vertical.getAsPercentage();
+ _scalar_scale_horizontal.setFromPercentage (scaleYPercentage);
+ }
+ }
+}
+
+void Transformation::onRotateValueChanged()
+{
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+}
+
+void Transformation::onRotateCounterclockwiseClicked()
+{
+ _scalar_rotate.setTooltipText(_("Rotation angle (positive = counterclockwise)"));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/rotateCounterClockwise", !getDesktop()->is_yaxisdown());
+}
+
+void Transformation::onRotateClockwiseClicked()
+{
+ _scalar_rotate.setTooltipText(_("Rotation angle (positive = clockwise)"));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/rotateCounterClockwise", getDesktop()->is_yaxisdown());
+}
+
+void Transformation::onSkewValueChanged()
+{
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+}
+
+void Transformation::onTransformValueChanged()
+{
+
+ /*
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue();
+ double f = _scalar_transform_f.getValue();
+
+ //g_message("onTransformValueChanged: (%f, %f, %f, %f, %f, %f)\n",
+ // a, b, c, d, e ,f);
+ */
+
+ setResponseSensitive(Gtk::RESPONSE_APPLY, true);
+}
+
+void Transformation::onReplaceMatrixToggled()
+{
+ Inkscape::Selection *selection = _getSelection();
+
+ if (!selection || selection->isEmpty())
+ return;
+
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue();
+ double f = _scalar_transform_f.getValue();
+
+ Geom::Affine displayed (a, b, c, d, e, f);
+ Geom::Affine current = selection->items().front()->transform; // take from the first item in selection
+
+ Geom::Affine new_displayed;
+ if (_check_replace_matrix.get_active()) {
+ new_displayed = current;
+ } else {
+ new_displayed = current.inverse() * displayed;
+ }
+
+ _scalar_transform_a.setValue(new_displayed[0]);
+ _scalar_transform_b.setValue(new_displayed[1]);
+ _scalar_transform_c.setValue(new_displayed[2]);
+ _scalar_transform_d.setValue(new_displayed[3]);
+ _scalar_transform_e.setValue(new_displayed[4]);
+ _scalar_transform_f.setValue(new_displayed[5]);
+}
+
+void Transformation::onScaleProportionalToggled()
+{
+ onScaleXValueChanged();
+ if (_scalar_scale_vertical.setProgrammatically) {
+ _scalar_scale_vertical.setProgrammatically = false;
+ }
+}
+
+
+void Transformation::onClear()
+{
+ int const page = _notebook.get_current_page();
+
+ switch (page) {
+ case PAGE_MOVE: {
+ Inkscape::Selection *selection = _getSelection();
+ if (!selection || selection->isEmpty() || _check_move_relative.get_active()) {
+ _scalar_move_horizontal.setValue(0);
+ _scalar_move_vertical.setValue(0);
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ _scalar_move_horizontal.setValue(bbox->min()[Geom::X], "px");
+ _scalar_move_vertical.setValue(bbox->min()[Geom::Y], "px");
+ }
+ }
+ break;
+ }
+ case PAGE_ROTATE: {
+ _scalar_rotate.setValue(0);
+ break;
+ }
+ case PAGE_SCALE: {
+ _scalar_scale_horizontal.setValue(100, "%");
+ _scalar_scale_vertical.setValue(100, "%");
+ break;
+ }
+ case PAGE_SKEW: {
+ _scalar_skew_horizontal.setValue(0);
+ _scalar_skew_vertical.setValue(0);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ _scalar_transform_a.setValue(1);
+ _scalar_transform_b.setValue(0);
+ _scalar_transform_c.setValue(0);
+ _scalar_transform_d.setValue(1);
+ _scalar_transform_e.setValue(0);
+ _scalar_transform_f.setValue(0);
+ break;
+ }
+ }
+}
+
+void Transformation::onApplySeparatelyToggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active());
+}
+
+
+} // 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/transformation.h b/src/ui/dialog/transformation.h
new file mode 100644
index 0000000..f50e214
--- /dev/null
+++ b/src/ui/dialog/transformation.h
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Transform dialog
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TRANSFORMATION_H
+#define INKSCAPE_UI_DIALOG_TRANSFORMATION_H
+
+
+#include <gtkmm/notebook.h>
+#include <glibmm/i18n.h>
+
+#include "ui/widget/panel.h"
+#include "ui/widget/notebook-page.h"
+#include "ui/widget/scalar-unit.h"
+#include "ui/dialog/desktop-tracker.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * Transformation dialog.
+ *
+ * The transformation dialog allows to modify Inkscape objects.
+ * 5 transformation operations are currently possible: move, scale,
+ * rotate, skew and matrix.
+ */
+class Transformation : public UI::Widget::Panel
+{
+
+public:
+
+ /**
+ * Constructor for Transformation.
+ *
+ * This does the initialization
+ * and layout of the dialog used for transforming SVG objects. It
+ * consists of 5 pages for the 5 operations it handles:
+ * 'Move' allows x,y translation of SVG objects
+ * 'Scale' allows linear resizing of SVG objects
+ * 'Rotate' allows rotating SVG objects by a degree
+ * 'Skew' allows skewing SVG objects
+ * 'Matrix' allows applying a generic affine transform on SVG objects,
+ * with the user specifying the 6 degrees of freedom manually.
+ *
+ * The dialog is implemented as a Gtk::Notebook with five pages.
+ * The pages are implemented using Inkscape's NotebookPage which
+ * is used to help make sure all of Inkscape's notebooks follow
+ * the same style. We then populate the pages with our widgets,
+ * we use the ScalarUnit class for this.
+ */
+ Transformation();
+
+ /**
+ * Cleanup
+ */
+ ~Transformation() override;
+
+ /**
+ * Factory method. Create an instance of this class/interface
+ */
+ static Transformation &getInstance()
+ { return *new Transformation(); }
+
+
+ /**
+ * Show the Move panel
+ */
+ void setPageMove()
+ { presentPage(PAGE_MOVE); }
+
+
+ /**
+ * Show the Scale panel
+ */
+ void setPageScale()
+ { presentPage(PAGE_SCALE); }
+
+
+ /**
+ * Show the Rotate panel
+ */
+ void setPageRotate()
+ { presentPage(PAGE_ROTATE); }
+
+ /**
+ * Show the Skew panel
+ */
+ void setPageSkew()
+ { presentPage(PAGE_SKEW); }
+
+ /**
+ * Show the Transform panel
+ */
+ void setPageTransform()
+ { presentPage(PAGE_TRANSFORM); }
+
+
+ int getCurrentPage()
+ { return _notebook.get_current_page(); }
+
+ enum PageType {
+ PAGE_MOVE, PAGE_SCALE, PAGE_ROTATE, PAGE_SKEW, PAGE_TRANSFORM, PAGE_QTY
+ };
+
+ void updateSelection(PageType page, Inkscape::Selection *selection);
+
+protected:
+
+ Gtk::Notebook _notebook;
+
+ UI::Widget::NotebookPage _page_move;
+ UI::Widget::NotebookPage _page_scale;
+ UI::Widget::NotebookPage _page_rotate;
+ UI::Widget::NotebookPage _page_skew;
+ UI::Widget::NotebookPage _page_transform;
+
+ UI::Widget::UnitMenu _units_move;
+ UI::Widget::UnitMenu _units_scale;
+ UI::Widget::UnitMenu _units_rotate;
+ UI::Widget::UnitMenu _units_skew;
+
+ UI::Widget::ScalarUnit _scalar_move_horizontal;
+ UI::Widget::ScalarUnit _scalar_move_vertical;
+ UI::Widget::ScalarUnit _scalar_scale_horizontal;
+ UI::Widget::ScalarUnit _scalar_scale_vertical;
+ UI::Widget::ScalarUnit _scalar_rotate;
+ UI::Widget::ScalarUnit _scalar_skew_horizontal;
+ UI::Widget::ScalarUnit _scalar_skew_vertical;
+
+ UI::Widget::Scalar _scalar_transform_a;
+ UI::Widget::Scalar _scalar_transform_b;
+ UI::Widget::Scalar _scalar_transform_c;
+ UI::Widget::Scalar _scalar_transform_d;
+ UI::Widget::Scalar _scalar_transform_e;
+ UI::Widget::Scalar _scalar_transform_f;
+
+ Gtk::RadioButton _counterclockwise_rotate;
+ Gtk::RadioButton _clockwise_rotate;
+
+ Gtk::CheckButton _check_move_relative;
+ Gtk::CheckButton _check_scale_proportional;
+ Gtk::CheckButton _check_apply_separately;
+ Gtk::CheckButton _check_replace_matrix;
+
+ SPDesktop *_desktop;
+ DesktopTracker _deskTrack;
+ sigc::connection _desktopChangeConn;
+
+ /**
+ * Layout the GUI components, and prepare for use
+ */
+ void layoutPageMove();
+ void layoutPageScale();
+ void layoutPageRotate();
+ void layoutPageSkew();
+ void layoutPageTransform();
+
+ void _apply() override;
+ void presentPage(PageType page);
+
+ void onSwitchPage(Gtk::Widget *page, guint pagenum);
+
+ /**
+ * Callbacks for when a user changes values on the panels
+ */
+ void onMoveValueChanged();
+ void onMoveRelativeToggled();
+ void onScaleXValueChanged();
+ void onScaleYValueChanged();
+ void onRotateValueChanged();
+ void onRotateCounterclockwiseClicked();
+ void onRotateClockwiseClicked();
+ void onSkewValueChanged();
+ void onTransformValueChanged();
+ void onReplaceMatrixToggled();
+ void onScaleProportionalToggled();
+
+ void onClear();
+
+ void onApplySeparatelyToggled();
+
+ /**
+ * Called when the selection is updated, to make
+ * the panel(s) show the new values.
+ * Editor---->dialog
+ */
+ void updatePageMove(Inkscape::Selection *);
+ void updatePageScale(Inkscape::Selection *);
+ void updatePageRotate(Inkscape::Selection *);
+ void updatePageSkew(Inkscape::Selection *);
+ void updatePageTransform(Inkscape::Selection *);
+
+ /**
+ * Called when the Apply button is pushed
+ * Dialog---->editor
+ */
+ void applyPageMove(Inkscape::Selection *);
+ void applyPageScale(Inkscape::Selection *);
+ void applyPageRotate(Inkscape::Selection *);
+ void applyPageSkew(Inkscape::Selection *);
+ void applyPageTransform(Inkscape::Selection *);
+
+ void setTargetDesktop(SPDesktop* desktop);
+
+private:
+
+ /**
+ * Copy constructor
+ */
+ Transformation(Transformation const &d) = delete;
+
+ /**
+ * Assignment operator
+ */
+ Transformation operator=(Transformation const &d) = delete;
+
+ Gtk::Button *applyButton;
+ Gtk::Button *resetButton;
+ Gtk::Button *cancelButton;
+
+ sigc::connection _selChangeConn;
+ sigc::connection _selModifyConn;
+};
+
+
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+
+
+#endif //INKSCAPE_UI_DIALOG_TRANSFORMATION_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/undo-history.cpp b/src/ui/dialog/undo-history.cpp
new file mode 100644
index 0000000..eae9ea6
--- /dev/null
+++ b/src/ui/dialog/undo-history.cpp
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Undo History dialog - implementation.
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "undo-history.h"
+
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "ui/icon-loader.h"
+#include "util/signal-blocker.h"
+
+#include "desktop.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/* Rendering functions for custom cell renderers */
+void CellRendererSPIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags)
+{
+ // if this event type doesn't have an icon...
+ if ( !Inkscape::Verb::get(_property_event_type)->get_image() ) return;
+
+ // if the icon isn't cached, render it to a pixbuf
+ if ( !_icon_cache[_property_event_type] ) {
+
+ Glib::ustring image_name = Inkscape::Verb::get(_property_event_type)->get_image();
+ Gtk::Image* icon = Gtk::manage(new Gtk::Image());
+ icon = sp_get_icon_image(image_name, Gtk::ICON_SIZE_MENU);
+
+ if (icon) {
+
+ // check icon type (inkscape, gtk, none)
+ if ( GTK_IS_IMAGE(icon->gobj()) ) {
+ _property_icon = sp_get_icon_pixbuf(image_name, 16);
+ } else {
+ delete icon;
+ return;
+ }
+
+ delete icon;
+ property_pixbuf() = _icon_cache[_property_event_type] = _property_icon.get_value();
+ }
+
+ } else {
+ property_pixbuf() = _icon_cache[_property_event_type];
+ }
+
+ Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area,
+ cell_area, flags);
+}
+
+
+void CellRendererInt::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags)
+{
+ if( _filter(_property_number) ) {
+ std::ostringstream s;
+ s << _property_number << std::flush;
+ property_text() = s.str();
+ Gtk::CellRendererText::render_vfunc(cr, widget, background_area,
+ cell_area, flags);
+ }
+}
+
+const CellRendererInt::Filter& CellRendererInt::no_filter = CellRendererInt::NoFilter();
+
+UndoHistory& UndoHistory::getInstance()
+{
+ return *new UndoHistory();
+}
+
+UndoHistory::UndoHistory()
+ : UI::Widget::Panel("/dialogs/undo-history", SP_VERB_DIALOG_UNDO_HISTORY),
+ _document_replaced_connection(),
+ _desktop(getDesktop()),
+ _document(_desktop ? _desktop->doc() : nullptr),
+ _event_log(_desktop ? _desktop->event_log : nullptr),
+ _columns(_event_log ? &_event_log->getColumns() : nullptr),
+ _scrolled_window(),
+ _event_list_store(),
+ _event_list_selection(_event_list_view.get_selection()),
+ _deskTrack(),
+ _desktopChangeConn(),
+ _callback_connections()
+{
+ if ( !_document || !_event_log || !_columns ) return;
+
+ set_size_request(-1, 95);
+
+ _getContents()->pack_start(_scrolled_window);
+ _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+
+ // connect with the EventLog
+ _connectEventLog();
+
+ _event_list_view.set_enable_search(false);
+ _event_list_view.set_headers_visible(false);
+
+ CellRendererSPIcon* icon_renderer = Gtk::manage(new CellRendererSPIcon());
+ icon_renderer->property_xpad() = 2;
+ icon_renderer->property_width() = 24;
+ int cols_count = _event_list_view.append_column("Icon", *icon_renderer);
+
+ Gtk::TreeView::Column* icon_column = _event_list_view.get_column(cols_count-1);
+ icon_column->add_attribute(icon_renderer->property_event_type(), _columns->type);
+
+ CellRendererInt* children_renderer = Gtk::manage(new CellRendererInt(greater_than_1));
+ children_renderer->property_weight() = 600; // =Pango::WEIGHT_SEMIBOLD (not defined in old versions of pangomm)
+ children_renderer->property_xalign() = 1.0;
+ children_renderer->property_xpad() = 2;
+ children_renderer->property_width() = 24;
+
+ cols_count = _event_list_view.append_column("Children", *children_renderer);
+ Gtk::TreeView::Column* children_column = _event_list_view.get_column(cols_count-1);
+ children_column->add_attribute(children_renderer->property_number(), _columns->child_count);
+
+ Gtk::CellRendererText* description_renderer = Gtk::manage(new Gtk::CellRendererText());
+ description_renderer->property_ellipsize() = Pango::ELLIPSIZE_END;
+
+ cols_count = _event_list_view.append_column("Description", *description_renderer);
+ Gtk::TreeView::Column* description_column = _event_list_view.get_column(cols_count-1);
+ description_column->add_attribute(description_renderer->property_text(), _columns->description);
+ description_column->set_resizable();
+ description_column->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
+ description_column->set_min_width (150);
+
+ _event_list_view.set_expander_column( *_event_list_view.get_column(cols_count-1) );
+
+ _scrolled_window.add(_event_list_view);
+
+ // connect EventLog callbacks
+ _callback_connections[EventLog::CALLB_SELECTION_CHANGE] =
+ _event_list_selection->signal_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onListSelectionChange));
+
+ _callback_connections[EventLog::CALLB_EXPAND] =
+ _event_list_view.signal_row_expanded().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onExpandEvent));
+
+ _callback_connections[EventLog::CALLB_COLLAPSE] =
+ _event_list_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onCollapseEvent));
+
+ _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &UndoHistory::setDesktop) );
+ _deskTrack.connect(GTK_WIDGET(gobj()));
+
+ // connect to be informed of document changes
+ signalDocumentReplaced().connect(sigc::mem_fun(*this, &UndoHistory::_handleDocumentReplaced));
+
+ show_all_children();
+
+ // scroll to the selected row
+ _event_list_view.set_cursor(_event_list_store->get_path(_event_log->getCurrEvent()));
+}
+
+UndoHistory::~UndoHistory()
+{
+ _desktopChangeConn.disconnect();
+}
+
+
+void UndoHistory::setDesktop(SPDesktop* desktop)
+{
+ Panel::setDesktop(desktop);
+
+ EventLog *newEventLog = desktop ? desktop->event_log : nullptr;
+ if ((_desktop == desktop) && (_event_log == newEventLog)) {
+ // same desktop set
+ }
+ else
+ {
+ _connectDocument(desktop, desktop ? desktop->doc() : nullptr);
+ }
+}
+
+void UndoHistory::_connectDocument(SPDesktop* desktop, SPDocument * /*document*/)
+{
+ // disconnect from prior
+ if (_event_log) {
+ _event_log->removeDialogConnection(&_event_list_view, &_callback_connections);
+ }
+
+ SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);
+
+ _event_list_view.unset_model();
+
+ // connect to new EventLog/Desktop
+ _desktop = desktop;
+ _event_log = desktop ? desktop->event_log : nullptr;
+ _document = desktop ? desktop->doc() : nullptr;
+ _connectEventLog();
+}
+
+void UndoHistory::_connectEventLog()
+{
+ if (_event_log) {
+ _event_log->add_destroy_notify_callback(this, &_handleEventLogDestroyCB);
+ _event_list_store = _event_log->getEventListStore();
+
+ _event_list_view.set_model(_event_list_store);
+
+ _event_log->addDialogConnection(&_event_list_view, &_callback_connections);
+ _event_list_view.scroll_to_row(_event_list_store->get_path(_event_list_selection->get_selected()));
+ }
+}
+
+void UndoHistory::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document)
+{
+ if ((desktop != _desktop) || (document != _document)) {
+ _connectDocument(desktop, document);
+ }
+}
+
+void *UndoHistory::_handleEventLogDestroyCB(void *data)
+{
+ void *result = nullptr;
+ if (data) {
+ UndoHistory *self = reinterpret_cast<UndoHistory*>(data);
+ result = self->_handleEventLogDestroy();
+ }
+ return result;
+}
+
+// called *after* _event_log has been destroyed.
+void *UndoHistory::_handleEventLogDestroy()
+{
+ if (_event_log) {
+ SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);
+
+ _event_list_view.unset_model();
+ _event_list_store.reset();
+ _event_log = nullptr;
+ }
+
+ return nullptr;
+}
+
+void
+UndoHistory::_onListSelectionChange()
+{
+
+ EventLog::const_iterator selected = _event_list_selection->get_selected();
+
+ /* If no event is selected in the view, find the right one and select it. This happens whenever
+ * a branch we're currently in is collapsed.
+ */
+ if (!selected) {
+
+ EventLog::iterator curr_event = _event_log->getCurrEvent();
+
+ if (curr_event->parent()) {
+
+ EventLog::iterator curr_event_parent = curr_event->parent();
+ EventLog::iterator last = curr_event_parent->children().end();
+
+ _event_log->blockNotifications();
+ for ( --last ; curr_event != last ; ++curr_event ) {
+ DocumentUndo::redo(_document);
+ }
+ _event_log->blockNotifications(false);
+
+ _event_log->setCurrEvent(curr_event);
+ _event_list_selection->select(curr_event_parent);
+
+ } else { // this should not happen
+ _event_list_selection->select(curr_event);
+ }
+
+ } else {
+
+ EventLog::const_iterator last_selected = _event_log->getCurrEvent();
+
+ /* Selecting a collapsed parent event is equal to selecting the last child
+ * of that parent's branch.
+ */
+
+ if ( !selected->children().empty() &&
+ !_event_list_view.row_expanded(_event_list_store->get_path(selected)) )
+ {
+ selected = selected->children().end();
+ --selected;
+ }
+
+ // An event before the current one has been selected. Undo to the selected event.
+ if ( _event_list_store->get_path(selected) <
+ _event_list_store->get_path(last_selected) )
+ {
+ _event_log->blockNotifications();
+
+ while ( selected != last_selected ) {
+
+ DocumentUndo::undo(_document);
+
+ if ( last_selected->parent() &&
+ last_selected == last_selected->parent()->children().begin() )
+ {
+ last_selected = last_selected->parent();
+ _event_log->setCurrEventParent((EventLog::iterator)nullptr);
+ } else {
+ --last_selected;
+ if ( !last_selected->children().empty() ) {
+ _event_log->setCurrEventParent(last_selected);
+ last_selected = last_selected->children().end();
+ --last_selected;
+ }
+ }
+ }
+ _event_log->blockNotifications(false);
+ _event_log->updateUndoVerbs();
+
+ } else { // An event after the current one has been selected. Redo to the selected event.
+
+ _event_log->blockNotifications();
+
+ while ( selected != last_selected ) {
+
+ DocumentUndo::redo(_document);
+
+ if ( !last_selected->children().empty() ) {
+ _event_log->setCurrEventParent(last_selected);
+ last_selected = last_selected->children().begin();
+ } else {
+ ++last_selected;
+ if ( last_selected->parent() &&
+ last_selected == last_selected->parent()->children().end() )
+ {
+ last_selected = last_selected->parent();
+ ++last_selected;
+ _event_log->setCurrEventParent((EventLog::iterator)nullptr);
+ }
+ }
+ }
+ _event_log->blockNotifications(false);
+
+ }
+
+ _event_log->setCurrEvent(selected);
+ _event_log->updateUndoVerbs();
+ }
+
+}
+
+void
+UndoHistory::_onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
+{
+ if ( iter == _event_list_selection->get_selected() ) {
+ _event_list_selection->select(_event_log->getCurrEvent());
+ }
+}
+
+void
+UndoHistory::_onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
+{
+ // Collapsing a branch we're currently in is equal to stepping to the last event in that branch
+ if ( iter == _event_log->getCurrEvent() ) {
+ EventLog::const_iterator curr_event_parent = _event_log->getCurrEvent();
+ EventLog::const_iterator curr_event = curr_event_parent->children().begin();
+ EventLog::const_iterator last = curr_event_parent->children().end();
+
+ _event_log->blockNotifications();
+ DocumentUndo::redo(_document);
+
+ for ( --last ; curr_event != last ; ++curr_event ) {
+ DocumentUndo::redo(_document);
+ }
+ _event_log->blockNotifications(false);
+
+ _event_log->setCurrEvent(curr_event);
+ _event_log->setCurrEventParent(curr_event_parent);
+ _event_list_selection->select(curr_event_parent);
+ }
+}
+
+const CellRendererInt::Filter& UndoHistory::greater_than_1 = UndoHistory::GreaterThan(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/undo-history.h b/src/ui/dialog/undo-history.h
new file mode 100644
index 0000000..244980b
--- /dev/null
+++ b/src/ui/dialog/undo-history.h
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Undo History dialog
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_UNDO_HISTORY_H
+#define INKSCAPE_UI_DIALOG_UNDO_HISTORY_H
+
+#include "ui/widget/panel.h"
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treeselection.h>
+#include <glibmm/property.h>
+
+#include <functional>
+#include <sstream>
+
+#include "event-log.h"
+
+#include "ui/dialog/desktop-tracker.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/* Custom cell renderers */
+
+class CellRendererSPIcon : public Gtk::CellRendererPixbuf {
+public:
+
+ CellRendererSPIcon() :
+ Glib::ObjectBase(typeid(CellRendererPixbuf)),
+ Gtk::CellRendererPixbuf(),
+ _property_icon(*this, "icon", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_event_type(*this, "event_type", 0)
+ { }
+
+ Glib::PropertyProxy<unsigned int>
+ property_event_type() { return _property_event_type.get_proxy(); }
+
+protected:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override;
+private:
+
+ Glib::Property<Glib::RefPtr<Gdk::Pixbuf> > _property_icon;
+ Glib::Property<unsigned int> _property_event_type;
+ std::map<const unsigned int, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache;
+
+};
+
+
+class CellRendererInt : public Gtk::CellRendererText {
+public:
+
+ struct Filter : std::unary_function<int, bool> {
+ virtual ~Filter() = default;
+ virtual bool operator() (const int&) const =0;
+ };
+
+ CellRendererInt(const Filter& filter=no_filter) :
+ Glib::ObjectBase(typeid(CellRendererText)),
+ Gtk::CellRendererText(),
+ _property_number(*this, "number", 0),
+ _filter (filter)
+ { }
+
+
+ Glib::PropertyProxy<int>
+ property_number() { return _property_number.get_proxy(); }
+
+ static const Filter& no_filter;
+
+protected:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override;
+
+private:
+
+ Glib::Property<int> _property_number;
+ const Filter& _filter;
+
+ struct NoFilter : Filter { bool operator() (const int& /*x*/) const override { return true; } };
+};
+
+/**
+ * \brief Dialog for presenting document change history
+ *
+ * This dialog allows the user to undo and redo multiple events in a more convenient way
+ * than repateaded ctrl-z, ctrl-shift-z.
+ */
+class UndoHistory : public Widget::Panel {
+public:
+ ~UndoHistory() override;
+
+ static UndoHistory &getInstance();
+ void setDesktop(SPDesktop* desktop) override;
+
+ sigc::connection _document_replaced_connection;
+
+protected:
+
+ SPDesktop *_desktop;
+ SPDocument *_document;
+ EventLog *_event_log;
+
+
+ const EventLog::EventModelColumns *_columns;
+
+ Gtk::ScrolledWindow _scrolled_window;
+
+ Glib::RefPtr<Gtk::TreeModel> _event_list_store;
+ Gtk::TreeView _event_list_view;
+ Glib::RefPtr<Gtk::TreeSelection> _event_list_selection;
+
+ DesktopTracker _deskTrack;
+ sigc::connection _desktopChangeConn;
+
+ EventLog::CallbackMap _callback_connections;
+
+ static void *_handleEventLogDestroyCB(void *data);
+
+ void _connectDocument(SPDesktop* desktop, SPDocument *document);
+ void _connectEventLog();
+ void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document);
+ void *_handleEventLogDestroy();
+ void _onListSelectionChange();
+ void _onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+
+private:
+ UndoHistory();
+
+ // no default constructor, noncopyable, nonassignable
+ UndoHistory(UndoHistory const &d) = delete;
+ UndoHistory operator=(UndoHistory const &d) = delete;
+
+ struct GreaterThan : CellRendererInt::Filter {
+ GreaterThan(int _i) : i (_i) {}
+ bool operator() (const int& x) const override { return x > i; }
+ int i;
+ };
+
+ static const CellRendererInt::Filter& greater_than_1;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_UNDO_HISTORY_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/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp
new file mode 100644
index 0000000..dcc5c55
--- /dev/null
+++ b/src/ui/dialog/xml-tree.cpp
@@ -0,0 +1,968 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * XML editor.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * David Turner
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2006 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include "xml-tree.h"
+
+#include <glibmm/i18n.h>
+#include <memory>
+
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "shortcuts.h"
+#include "verbs.h"
+
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+
+#include "widgets/sp-xmlview-tree.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+XmlTree::XmlTree()
+ : UI::Widget::Panel("/dialogs/xml/", SP_VERB_DIALOG_XML_EDITOR)
+ , blocked(0)
+ , _message_stack(nullptr)
+ , _message_context(nullptr)
+ , current_desktop(nullptr)
+ , current_document(nullptr)
+ , selected_attr(0)
+ , selected_repr(nullptr)
+ , tree(nullptr)
+ , status("")
+ , new_window(nullptr)
+ , _updating(false)
+{
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop) {
+ return;
+ }
+
+ Gtk::Box *root = _getContents();
+ Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ 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);
+ contents->pack_start(_paned, true, true, 0);
+ contents->set_valign(Gtk::ALIGN_FILL);
+ contents->child_property_fill(_paned);
+
+ _paned.set_vexpand(true);
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack));
+ _message_changed_connection = _message_stack->connectChanged(
+ sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj())));
+
+ /* tree view */
+ tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr));
+ gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") );
+
+ tree_toolbar.set_toolbar_style(Gtk::TOOLBAR_ICONS);
+
+ auto xml_element_new_icon = Gtk::manage(sp_get_icon_image("xml-element-new", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_element_new_button.set_icon_widget(*xml_element_new_icon);
+ xml_element_new_button.set_label(_("New element node"));
+ xml_element_new_button.set_tooltip_text(_("New element node"));
+ xml_element_new_button.set_sensitive(false);
+ tree_toolbar.add(xml_element_new_button);
+
+ auto xml_text_new_icon = Gtk::manage(sp_get_icon_image("xml-text-new", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_text_new_button.set_icon_widget(*xml_text_new_icon);
+ xml_text_new_button.set_label(_("New text node"));
+ xml_text_new_button.set_tooltip_text(_("New text node"));
+ xml_text_new_button.set_sensitive(false);
+ tree_toolbar.add(xml_text_new_button);
+
+ auto xml_node_duplicate_icon = Gtk::manage(sp_get_icon_image("xml-node-duplicate", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_node_duplicate_button.set_icon_widget(*xml_node_duplicate_icon);
+ xml_node_duplicate_button.set_label(_("Duplicate node"));
+ xml_node_duplicate_button.set_tooltip_text(_("Duplicate node"));
+ xml_node_duplicate_button.set_sensitive(false);
+ tree_toolbar.add(xml_node_duplicate_button);
+
+ tree_toolbar.add(separator);
+
+ auto xml_node_delete_icon = Gtk::manage(sp_get_icon_image("xml-node-delete", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_node_delete_button.set_icon_widget(*xml_node_delete_icon);
+ xml_node_delete_button.set_label(_("Delete node"));
+ xml_node_delete_button.set_tooltip_text(_("Delete node"));
+ xml_node_delete_button.set_sensitive(false);
+ tree_toolbar.add(xml_node_delete_button);
+
+ tree_toolbar.add(separator2);
+
+ auto format_indent_less_icon = Gtk::manage(sp_get_icon_image("format-indent-less", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ unindent_node_button.set_icon_widget(*format_indent_less_icon);
+ unindent_node_button.set_label(_("Unindent node"));
+ unindent_node_button.set_tooltip_text(_("Unindent node"));
+ unindent_node_button.set_sensitive(false);
+ tree_toolbar.add(unindent_node_button);
+
+ auto format_indent_more_icon = Gtk::manage(sp_get_icon_image("format-indent-more", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ indent_node_button.set_icon_widget(*format_indent_more_icon);
+ indent_node_button.set_label(_("Indent node"));
+ indent_node_button.set_tooltip_text(_("Indent node"));
+ indent_node_button.set_sensitive(false);
+ tree_toolbar.add(indent_node_button);
+
+ auto go_up_icon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ raise_node_button.set_icon_widget(*go_up_icon);
+ raise_node_button.set_label(_("Raise node"));
+ raise_node_button.set_tooltip_text(_("Raise node"));
+ raise_node_button.set_sensitive(false);
+ tree_toolbar.add(raise_node_button);
+
+ auto go_down_icon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ lower_node_button.set_icon_widget(*go_down_icon);
+ lower_node_button.set_label(_("Lower node"));
+ lower_node_button.set_tooltip_text(_("Lower node"));
+ lower_node_button.set_sensitive(false);
+ tree_toolbar.add(lower_node_button);
+
+ node_box.pack_start(tree_toolbar, FALSE, TRUE, 0);
+
+ Gtk::ScrolledWindow *tree_scroller = new Gtk::ScrolledWindow();
+ tree_scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ tree_scroller->set_shadow_type(Gtk::SHADOW_IN);
+ tree_scroller->add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree))));
+
+ node_box.pack_start(*Gtk::manage(tree_scroller));
+
+ node_box.pack_end(status_box, false, false, 2);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool attrtoggler = prefs->getBool("/dialogs/xml/attrtoggler", true);
+ bool dir = prefs->getBool("/dialogs/xml/vertical", true);
+ attributes = new AttrDialog();
+ _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+ _paned.check_resize();
+ _paned.set_wide_handle(true);
+ _paned.pack1(node_box, false, false);
+ /* attributes */
+ Gtk::Box *actionsbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ actionsbox->set_valign(Gtk::ALIGN_START);
+ Gtk::Label *attrtogglerlabel = Gtk::manage(new Gtk::Label(_("Show attributes")));
+ attrtogglerlabel->set_margin_right(5);
+ _attrswitch.get_style_context()->add_class("inkswitch");
+ _attrswitch.get_style_context()->add_class("rawstyle");
+ _attrswitch.property_active() = attrtoggler;
+ _attrswitch.property_active().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_attrtoggler));
+ attrtogglerlabel->get_style_context()->add_class("inksmall");
+ actionsbox->pack_start(*attrtogglerlabel, Gtk::PACK_SHRINK);
+ actionsbox->pack_start(_attrswitch, 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, &XmlTree::_toggleDirection), _vertical));
+ _horizontal->property_draw_indicator() = false;
+ _vertical->property_draw_indicator() = false;
+ actionsbox->pack_end(*_horizontal, false, false, 0);
+ actionsbox->pack_end(*_vertical, false, false, 0);
+ _paned.pack2(*attributes, true, false);
+ contents->pack_start(*actionsbox, false, false, 0);
+ /* Signal handlers */
+ GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree));
+ g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK (on_tree_select_row), this);
+ g_signal_connect_after( G_OBJECT(tree), "tree_move", G_CALLBACK(after_tree_move), this);
+
+ xml_element_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_element_node));
+ xml_text_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_text_node));
+ xml_node_duplicate_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_duplicate_node));
+ xml_node_delete_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_delete_node));
+ unindent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_unindent_node));
+ indent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_indent_node));
+ raise_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_raise_node));
+ lower_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_lower_node));
+
+ desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &XmlTree::set_tree_desktop) );
+ deskTrack.connect(GTK_WIDGET(gobj()));
+ set_name("XMLAndAttributesDialog");
+ set_spacing(0);
+ set_size_request(320, 260);
+ show_all();
+
+ int panedpos = prefs->getInt("/dialogs/xml/panedpos", 200);
+ _paned.property_position() = panedpos;
+ _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_resized));
+
+ tree_reset_context();
+ root->pack_start(*Gtk::manage(contents), true, true);
+ g_assert(desktop != nullptr);
+ set_tree_desktop(desktop);
+
+}
+
+void XmlTree::_resized()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::_toggleDirection(Gtk::RadioButton *vertical)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dir = vertical->get_active();
+ prefs->setBool("/dialogs/xml/vertical", dir);
+ _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+ _paned.check_resize();
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::_attrtoggler()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool attrtoggler = !prefs->getBool("/dialogs/xml/attrtoggler", true);
+ prefs->setBool("/dialogs/xml/attrtoggler", attrtoggler);
+ if (attrtoggler) {
+ attributes->show();
+ } else {
+ attributes->hide();
+ }
+}
+
+void XmlTree::present()
+{
+ set_tree_select(get_dt_select());
+
+ UI::Widget::Panel::present();
+
+ if (!_attrswitch.property_active()) {
+ attributes->hide();
+ }
+}
+
+XmlTree::~XmlTree ()
+{
+ set_tree_desktop(nullptr);
+ if (current_desktop) {
+ current_desktop->getDocument()->setXMLDialogSelectedObject(nullptr);
+ }
+ _message_changed_connection.disconnect();
+ _message_context = nullptr;
+ _message_stack = nullptr;
+ _message_changed_connection.~connection();
+}
+
+void XmlTree::setDesktop(SPDesktop *desktop)
+{
+ Panel::setDesktop(desktop);
+ deskTrack.setBase(desktop);
+}
+
+/**
+ * Sets the XML status bar when the tree is selected.
+ */
+void XmlTree::tree_reset_context()
+{
+ _message_context->set(Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to select nodes, <b>drag</b> to rearrange."));
+}
+
+
+void XmlTree::set_tree_desktop(SPDesktop *desktop)
+{
+ if ( desktop == current_desktop ) {
+ return;
+ }
+
+ if (current_desktop) {
+ sel_changed_connection.disconnect();
+ document_replaced_connection.disconnect();
+ }
+ current_desktop = desktop;
+ if (desktop) {
+ sel_changed_connection = desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed)));
+ document_replaced_connection = desktop->connectDocumentReplaced(sigc::mem_fun(this, &XmlTree::on_document_replaced));
+
+ set_tree_document(desktop->getDocument());
+ } else {
+ set_tree_document(nullptr);
+ }
+
+} // end of set_tree_desktop()
+
+
+void XmlTree::set_tree_document(SPDocument *document)
+{
+ if (document == current_document) {
+ return;
+ }
+
+ if (current_document) {
+ document_uri_set_connection.disconnect();
+ }
+ current_document = document;
+ if (current_document) {
+
+ document_uri_set_connection = current_document->connectURISet(sigc::bind(sigc::ptr_fun(&on_document_uri_set), current_document));
+ on_document_uri_set( current_document->getDocumentURI(), current_document );
+ set_tree_repr(current_document->getReprRoot());
+ } else {
+ set_tree_repr(nullptr);
+ }
+}
+
+
+
+void XmlTree::set_tree_repr(Inkscape::XML::Node *repr)
+{
+ if (repr == selected_repr) {
+ return;
+ }
+
+ sp_xmlview_tree_set_repr(tree, repr);
+ if (repr) {
+ set_tree_select(get_dt_select());
+ } else {
+ set_tree_select(nullptr);
+ }
+
+ propagate_tree_select(selected_repr);
+
+}
+
+/**
+ * Expand all parent nodes of `repr`
+ */
+static void expand_parents(SPXMLViewTree *tree, Inkscape::XML::Node *repr)
+{
+ auto parentrepr = repr->parent();
+ if (!parentrepr) {
+ return;
+ }
+
+ expand_parents(tree, parentrepr);
+
+ GtkTreeIter node;
+ if (sp_xmlview_tree_get_repr_node(tree, parentrepr, &node)) {
+ GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node);
+ if (path) {
+ gtk_tree_view_expand_row(GTK_TREE_VIEW(tree), path, false);
+ }
+ }
+}
+
+void XmlTree::set_tree_select(Inkscape::XML::Node *repr)
+{
+ if (selected_repr) {
+ Inkscape::GC::release(selected_repr);
+ }
+
+ selected_repr = repr;
+ if (current_desktop) {
+ current_desktop->getDocument()->setXMLDialogSelectedObject(nullptr);
+ }
+ if (repr) {
+ GtkTreeIter node;
+
+ Inkscape::GC::anchor(selected_repr);
+
+ expand_parents(tree, repr);
+
+ if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), repr, &node)) {
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_unselect_all (selection);
+
+ GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node);
+ gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree), path, nullptr, TRUE, 0.66, 0.0);
+ gtk_tree_selection_select_iter(selection, &node);
+ gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, NULL, false);
+ gtk_tree_path_free(path);
+
+ } else {
+ g_message("XmlTree::set_tree_select : Couldn't find repr node");
+ }
+ } else {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_unselect_all (selection);
+
+ on_tree_unselect_row_disable();
+ }
+ propagate_tree_select(repr);
+}
+
+
+
+void XmlTree::propagate_tree_select(Inkscape::XML::Node *repr)
+{
+ if (repr &&
+ (repr->type() == Inkscape::XML::ELEMENT_NODE ||
+ repr->type() == Inkscape::XML::TEXT_NODE ||
+ repr->type() == Inkscape::XML::COMMENT_NODE))
+ {
+ attributes->setRepr(repr);
+ } else {
+ attributes->setRepr(nullptr);
+ }
+}
+
+
+Inkscape::XML::Node *XmlTree::get_dt_select()
+{
+ if (!current_desktop) {
+ return nullptr;
+ }
+ return current_desktop->getSelection()->singleRepr();
+}
+
+
+
+void XmlTree::set_dt_select(Inkscape::XML::Node *repr)
+{
+ if (!current_desktop) {
+ return;
+ }
+
+ Inkscape::Selection *selection = current_desktop->getSelection();
+
+ SPObject *object;
+ if (repr) {
+ while ( ( repr->type() != Inkscape::XML::ELEMENT_NODE )
+ && repr->parent() )
+ {
+ repr = repr->parent();
+ } // end of while loop
+
+ object = current_desktop->getDocument()->getObjectByRepr(repr);
+ } else {
+ object = nullptr;
+ }
+
+ blocked++;
+ if ( object && in_dt_coordsys(*object)
+ && !(SP_IS_STRING(object) ||
+ current_desktop->isLayer(object) ||
+ SP_IS_ROOT(object) ) )
+ {
+ /* We cannot set selection to root or string - they are not items and selection is not
+ * equipped to deal with them */
+ selection->set(SP_ITEM(object));
+ }
+
+ current_desktop->getDocument()->setXMLDialogSelectedObject(object);
+
+ blocked--;
+
+} // end of set_dt_select()
+
+
+void XmlTree::on_tree_select_row(GtkTreeSelection *selection, gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+
+ if (self->blocked) {
+ return;
+ }
+
+ // Defer the update after all events have been processed. Allows skipping
+ // of invalid intermediate selection states, like the automatic next row
+ // selection after `gtk_tree_store_remove`.
+ if (self->deferred_on_tree_select_row_id == 0) {
+ self->deferred_on_tree_select_row_id = //
+ g_idle_add(XmlTree::deferred_on_tree_select_row, data);
+ }
+}
+
+gboolean XmlTree::deferred_on_tree_select_row(gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+
+ self->deferred_on_tree_select_row_id = 0;
+
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+
+ if (self->selected_repr) {
+ Inkscape::GC::release(self->selected_repr);
+ self->selected_repr = nullptr;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->tree));
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
+ // Nothing selected, update widgets
+ self->propagate_tree_select(nullptr);
+ self->set_dt_select(nullptr);
+ self->on_tree_unselect_row_disable();
+ return FALSE;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter);
+ g_assert(repr != nullptr);
+
+
+ self->selected_repr = repr;
+ Inkscape::GC::anchor(self->selected_repr);
+
+ self->propagate_tree_select(self->selected_repr);
+
+ self->set_dt_select(self->selected_repr);
+
+ self->tree_reset_context();
+
+ self->on_tree_select_row_enable(&iter);
+
+ return FALSE;
+}
+
+
+void XmlTree::after_tree_move(SPXMLViewTree * /*tree*/, gpointer value, gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+ guint val = GPOINTER_TO_UINT(value);
+
+ if (val) {
+ DocumentUndo::done(self->current_document, SP_VERB_DIALOG_XML_EDITOR,
+ Q_("Undo History / XML dialog|Drag XML subtree"));
+ } else {
+ //DocumentUndo::cancel(self->current_document);
+ /*
+ * There was a problem with drag & drop,
+ * data is probably not synchronized, so reload the tree
+ */
+ SPDocument *document = self->current_document;
+ self->set_tree_document(nullptr);
+ self->set_tree_document(document);
+ }
+}
+
+void XmlTree::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget)
+{
+ if (widget) {
+ gtk_label_set_markup(GTK_LABEL(widget), message ? message : "");
+ }
+}
+
+void XmlTree::on_tree_select_row_enable(GtkTreeIter *node)
+{
+ if (!node) {
+ return;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node);
+ Inkscape::XML::Node *parent=repr->parent();
+
+ //on_tree_select_row_enable_if_mutable
+ xml_node_duplicate_button.set_sensitive(xml_tree_node_mutable(node));
+ xml_node_delete_button.set_sensitive(xml_tree_node_mutable(node));
+
+ //on_tree_select_row_enable_if_element
+ if (repr->type() == Inkscape::XML::ELEMENT_NODE) {
+ xml_element_new_button.set_sensitive(true);
+ xml_text_new_button.set_sensitive(true);
+
+ } else {
+ xml_element_new_button.set_sensitive(false);
+ xml_text_new_button.set_sensitive(false);
+ }
+
+ //on_tree_select_row_enable_if_has_grandparent
+ {
+ GtkTreeIter parent;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) {
+ GtkTreeIter grandparent;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &grandparent, &parent)) {
+ unindent_node_button.set_sensitive(true);
+ } else {
+ unindent_node_button.set_sensitive(false);
+ }
+ } else {
+ unindent_node_button.set_sensitive(false);
+ }
+ }
+ // on_tree_select_row_enable_if_indentable
+ gboolean indentable = FALSE;
+
+ if (xml_tree_node_mutable(node)) {
+ Inkscape::XML::Node *prev;
+
+ if ( parent && repr != parent->firstChild() ) {
+ g_assert(parent->firstChild());
+
+ // skip to the child just before the current repr
+ for ( prev = parent->firstChild() ;
+ prev && prev->next() != repr ;
+ prev = prev->next() ){};
+
+ if (prev && (prev->type() == Inkscape::XML::ELEMENT_NODE)) {
+ indentable = TRUE;
+ }
+ }
+ }
+
+ indent_node_button.set_sensitive(indentable);
+
+ //on_tree_select_row_enable_if_not_first_child
+ {
+ if ( parent && repr != parent->firstChild() ) {
+ raise_node_button.set_sensitive(true);
+ } else {
+ raise_node_button.set_sensitive(false);
+ }
+ }
+
+ //on_tree_select_row_enable_if_not_last_child
+ {
+ if ( parent && (parent->parent() && repr->next())) {
+ lower_node_button.set_sensitive(true);
+ } else {
+ lower_node_button.set_sensitive(false);
+ }
+ }
+}
+
+
+gboolean XmlTree::xml_tree_node_mutable(GtkTreeIter *node)
+{
+ // top-level is immutable, obviously
+ GtkTreeIter parent;
+ if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) {
+ return false;
+ }
+
+
+ // if not in base level (where namedview, defs, etc go), we're mutable
+ GtkTreeIter child;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &child, &parent)) {
+ return true;
+ }
+
+ Inkscape::XML::Node *repr;
+ repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node);
+ g_assert(repr);
+
+ // don't let "defs" or "namedview" disappear
+ if ( !strcmp(repr->name(),"svg:defs") ||
+ !strcmp(repr->name(),"sodipodi:namedview") ) {
+ return false;
+ }
+
+ // everyone else is okay, I guess. :)
+ return true;
+}
+
+
+
+void XmlTree::on_tree_unselect_row_disable()
+{
+ xml_text_new_button.set_sensitive(false);
+ xml_element_new_button.set_sensitive(false);
+ xml_node_delete_button.set_sensitive(false);
+ xml_node_duplicate_button.set_sensitive(false);
+ unindent_node_button.set_sensitive(false);
+ indent_node_button.set_sensitive(false);
+ raise_node_button.set_sensitive(false);
+ lower_node_button.set_sensitive(false);
+}
+
+void XmlTree::onCreateNameChanged()
+{
+ Glib::ustring text = name_entry->get_text();
+ /* TODO: need to do checking a little more rigorous than this */
+ create_button->set_sensitive(!text.empty());
+}
+
+void XmlTree::on_desktop_selection_changed()
+{
+ if (!blocked++) {
+ Inkscape::XML::Node *node = get_dt_select();
+ set_tree_select(node);
+ }
+ blocked--;
+}
+
+void XmlTree::on_document_replaced(SPDesktop *dt, SPDocument *doc)
+{
+ if (current_desktop)
+ sel_changed_connection.disconnect();
+
+ sel_changed_connection = dt->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed)));
+ set_tree_document(doc);
+}
+
+void XmlTree::on_document_uri_set(gchar const * /*uri*/, SPDocument * /*document*/)
+{
+/*
+ * Seems to be no way to set the title on a docked dialog
+ gchar title[500];
+ sp_ui_dialog_title_string(Inkscape::Verb::get(SP_VERB_DIALOG_XML_EDITOR), title);
+ gchar *t = g_strdup_printf("%s: %s", document->getName(), title);
+ //gtk_window_set_title(GTK_WINDOW(dlg), t);
+ g_free(t);
+*/
+}
+
+gboolean XmlTree::quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Escape: // defocus
+ gtk_widget_destroy(w);
+ return TRUE;
+ case GDK_KEY_Return: // create
+ case GDK_KEY_KP_Enter:
+ gtk_widget_destroy(w);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void XmlTree::cmd_new_element_node()
+{
+ Gtk::Dialog dialog;
+ Gtk::Entry entry;
+
+ dialog.get_content_area()->pack_start(entry);
+ dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL);
+ dialog.add_button("Create", Gtk::RESPONSE_OK);
+ dialog.show_all();
+
+ int result = dialog.run();
+ if (result == Gtk::RESPONSE_OK) {
+ Glib::ustring new_name = entry.get_text();
+ if (!new_name.empty()) {
+ Inkscape::XML::Document *xml_doc = current_document->getReprDoc();
+ Inkscape::XML::Node *new_repr;
+ new_repr = xml_doc->createElement(new_name.c_str());
+ Inkscape::GC::release(new_repr);
+ selected_repr->appendChild(new_repr);
+ set_tree_select(new_repr);
+ set_dt_select(new_repr);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR,
+ Q_("Undo History / XML dialog|Create new element node"));
+ }
+ }
+} // end of cmd_new_element_node()
+
+
+void XmlTree::cmd_new_text_node()
+{
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Document *xml_doc = current_document->getReprDoc();
+ Inkscape::XML::Node *text = xml_doc->createTextNode("");
+ selected_repr->appendChild(text);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR,
+ Q_("Undo History / XML dialog|Create new text node"));
+
+ set_tree_select(text);
+ set_dt_select(text);
+}
+
+void XmlTree::cmd_duplicate_node()
+{
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+ Inkscape::XML::Node *dup = selected_repr->duplicate(parent->document());
+ parent->addChild(dup, selected_repr);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Duplicate node"));
+
+ GtkTreeIter node;
+
+ if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), dup, &node)) {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_select_iter(selection, &node);
+ }
+}
+
+void XmlTree::cmd_delete_node()
+{
+ g_assert(selected_repr != nullptr);
+
+ current_document->setXMLDialogSelectedObject(nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+
+ sp_repr_unparent(selected_repr);
+
+ if (parent) {
+ auto parentobject = current_document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Delete node"));
+}
+
+void XmlTree::cmd_raise_node()
+{
+ g_assert(selected_repr != nullptr);
+
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+ g_return_if_fail(parent != nullptr);
+ g_return_if_fail(parent->firstChild() != selected_repr);
+
+ Inkscape::XML::Node *ref = nullptr;
+ Inkscape::XML::Node *before = parent->firstChild();
+ while (before && (before->next() != selected_repr)) {
+ ref = before;
+ before = before->next();
+ }
+
+ parent->changeOrder(selected_repr, ref);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Raise node"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+
+
+void XmlTree::cmd_lower_node()
+{
+ g_assert(selected_repr != nullptr);
+
+ g_return_if_fail(selected_repr->next() != nullptr);
+ Inkscape::XML::Node *parent = selected_repr->parent();
+
+ parent->changeOrder(selected_repr, selected_repr->next());
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Lower node"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+void XmlTree::cmd_indent_node()
+{
+ Inkscape::XML::Node *repr = selected_repr;
+ g_assert(repr != nullptr);
+
+ Inkscape::XML::Node *parent = repr->parent();
+ g_return_if_fail(parent != nullptr);
+ g_return_if_fail(parent->firstChild() != repr);
+
+ Inkscape::XML::Node* prev = parent->firstChild();
+ while (prev && (prev->next() != repr)) {
+ prev = prev->next();
+ }
+ g_return_if_fail(prev != nullptr);
+ g_return_if_fail(prev->type() == Inkscape::XML::ELEMENT_NODE);
+
+ Inkscape::XML::Node* ref = nullptr;
+ if (prev->firstChild()) {
+ for( ref = prev->firstChild() ; ref->next() ; ref = ref->next() ){};
+ }
+
+ parent->removeChild(repr);
+ prev->addChild(repr, ref);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Indent node"));
+ set_tree_select(repr);
+ set_dt_select(repr);
+
+} // end of cmd_indent_node()
+
+
+
+void XmlTree::cmd_unindent_node()
+{
+ Inkscape::XML::Node *repr = selected_repr;
+ g_assert(repr != nullptr);
+
+ Inkscape::XML::Node *parent = repr->parent();
+ g_return_if_fail(parent);
+ Inkscape::XML::Node *grandparent = parent->parent();
+ g_return_if_fail(grandparent);
+
+ parent->removeChild(repr);
+ grandparent->addChild(repr, parent);
+
+ DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Unindent node"));
+
+ set_tree_select(repr);
+ set_dt_select(repr);
+
+} // end of cmd_unindent_node()
+
+/** Returns true iff \a item is suitable to be included in the selection, in particular
+ whether it has a bounding box in the desktop coordinate system for rendering resize handles.
+
+ Descendents of <defs> nodes (markers etc.) return false, for example.
+*/
+bool XmlTree::in_dt_coordsys(SPObject const &item)
+{
+ /* Definition based on sp_item_i2doc_affine. */
+ SPObject const *child = &item;
+ g_return_val_if_fail(child != nullptr, false);
+ for(;;) {
+ if (!SP_IS_ITEM(child)) {
+ return false;
+ }
+ SPObject const * const parent = child->parent;
+ if (parent == nullptr) {
+ break;
+ }
+ child = parent;
+ }
+ g_assert(SP_IS_ROOT(child));
+ /* Relevance: Otherwise, I'm not sure whether to return true or false. */
+ 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/xml-tree.h b/src/ui/dialog/xml-tree.h
new file mode 100644
index 0000000..b63417a
--- /dev/null
+++ b/src/ui/dialog/xml-tree.h
@@ -0,0 +1,265 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * This is XML tree editor, which allows direct modifying of all elements
+ * of Inkscape document, including foreign ones.
+ *//*
+ * Authors: see git history
+ * Lauris Kaplinski, 2000
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_DIALOGS_XML_TREE_H
+#define SEEN_UI_DIALOGS_XML_TREE_H
+
+#include <memory>
+
+#include "ui/widget/panel.h"
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/separatortoolitem.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/toolbar.h>
+
+#include "message.h"
+
+#include "ui/dialog/attrdialog.h"
+#include "ui/dialog/desktop-tracker.h"
+
+
+class SPDesktop;
+class SPObject;
+struct SPXMLViewAttrList;
+struct SPXMLViewContent;
+struct SPXMLViewTree;
+
+namespace Inkscape {
+class MessageStack;
+class MessageContext;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Dialog {
+
+/**
+ * A dialog widget to view and edit the document xml
+ *
+ */
+
+class XmlTree : public Widget::Panel {
+public:
+ XmlTree ();
+ ~XmlTree () override;
+
+ static XmlTree &getInstance() { return *new XmlTree(); }
+
+private:
+
+ /**
+ * Is invoked by the desktop tracker when the desktop changes.
+ */
+ void set_tree_desktop(SPDesktop *desktop);
+
+ /**
+ * Is invoked when the document changes
+ */
+ void set_tree_document(SPDocument *document);
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_repr(Inkscape::XML::Node *repr);
+
+ /**
+ * Sets the XML status bar when the tree is selected.
+ */
+ void tree_reset_context();
+
+ /**
+ * Is the selected tree node editable
+ */
+ gboolean xml_tree_node_mutable(GtkTreeIter *node);
+
+ /**
+ * Callback to close the add dialog on Escape key
+ */
+ static gboolean quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/);
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Set the attribute list to match the selected node in the tree
+ */
+ void propagate_tree_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Find the current desktop selection
+ */
+ Inkscape::XML::Node *get_dt_select();
+
+ /**
+ * Select the current desktop selection
+ */
+ void set_dt_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Callback for a node in the tree being selected
+ */
+ static void on_tree_select_row(GtkTreeSelection *selection, gpointer data);
+ /**
+ * Callback for deferring the `on_tree_select_row` response in order to
+ * skip invalid intermediate selection states. In particular,
+ * `gtk_tree_store_remove` makes an undesired selection that we will
+ * immediately revert and don't want to an early response for.
+ */
+ static gboolean deferred_on_tree_select_row(gpointer);
+ /// Event source ID for the last scheduled `deferred_on_tree_select_row` event.
+ guint deferred_on_tree_select_row_id = 0;
+
+ /**
+ * Callback when a node is moved in the tree
+ */
+ static void after_tree_move(SPXMLViewTree *tree, gpointer value, gpointer data);
+
+ /**
+ * Callback for when an attribute is edited.
+ */
+ //static void on_attr_edited(SPXMLViewAttrList *attributes, const gchar * name, const gchar * value, gpointer /*data*/);
+
+ /**
+ * Callback for when attribute list values change
+ */
+ //static void on_attr_row_changed(SPXMLViewAttrList *attributes, const gchar * name, gpointer data);
+
+ /**
+ * Enable widgets based on current selections
+ */
+ void on_tree_select_row_enable(GtkTreeIter *node);
+ void on_tree_unselect_row_disable();
+ void on_tree_unselect_row_hide();
+ void on_attr_unselect_row_disable();
+
+ void onNameChanged();
+ void onCreateNameChanged();
+
+ /**
+ * Callbacks for changes in desktop selection and current document
+ */
+ void on_desktop_selection_changed();
+ void on_document_replaced(SPDesktop *dt, SPDocument *document);
+ static void on_document_uri_set(gchar const *uri, SPDocument *document);
+
+ static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog);
+
+ /**
+ * Callbacks for toolbar buttons being pressed
+ */
+ void cmd_new_element_node();
+ void cmd_new_text_node();
+ void cmd_duplicate_node();
+ void cmd_delete_node();
+ void cmd_raise_node();
+ void cmd_lower_node();
+ void cmd_indent_node();
+ void cmd_unindent_node();
+
+ void present() override;
+ void _attrtoggler();
+ void _toggleDirection(Gtk::RadioButton *vertical);
+ void _resized();
+ bool in_dt_coordsys(SPObject const &item);
+
+ /**
+ * Can be invoked for setting the desktop. Currently not used.
+ */
+ void setDesktop(SPDesktop *desktop) override;
+
+ /**
+ * Flag to ensure only one operation is performed at once
+ */
+ gint blocked;
+
+ bool _updating;
+ /**
+ * Status bar
+ */
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _message_context;
+
+ /**
+ * Signal handlers
+ */
+ sigc::connection _message_changed_connection;
+ sigc::connection document_replaced_connection;
+ sigc::connection document_uri_set_connection;
+ sigc::connection sel_changed_connection;
+
+ /**
+ * Current document and desktop this dialog is attached to
+ */
+ SPDesktop *current_desktop;
+ SPDocument *current_document;
+
+ gint selected_attr;
+ Inkscape::XML::Node *selected_repr;
+
+ /* XmlTree Widgets */
+ SPXMLViewTree *tree;
+ //SPXMLViewAttrList *attributes;
+ AttrDialog *attributes;
+ Gtk::Box *_attrbox;
+
+ /* XML Node Creation pop-up window */
+ Gtk::Entry *name_entry;
+ Gtk::Button *create_button;
+ Gtk::Paned _paned;
+
+ Gtk::VBox node_box;
+ Gtk::HBox status_box;
+ Gtk::Switch _attrswitch;
+ Gtk::Label status;
+ Gtk::Toolbar tree_toolbar;
+ Gtk::ToolButton xml_element_new_button;
+ Gtk::ToolButton xml_text_new_button;
+ Gtk::ToolButton xml_node_delete_button;
+ Gtk::SeparatorToolItem separator;
+ Gtk::ToolButton xml_node_duplicate_button;
+ Gtk::SeparatorToolItem separator2;
+ Gtk::ToolButton unindent_node_button;
+ Gtk::ToolButton indent_node_button;
+ Gtk::ToolButton raise_node_button;
+ Gtk::ToolButton lower_node_button;
+
+ GtkWidget *new_window;
+
+ DesktopTracker deskTrack;
+ sigc::connection desktopChangeConn;
+};
+
+}
+}
+}
+
+#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/drag-and-drop.cpp b/src/ui/drag-and-drop.cpp
new file mode 100644
index 0000000..d96d3e6
--- /dev/null
+++ b/src/ui/drag-and-drop.cpp
@@ -0,0 +1,534 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/**
+ * @file
+ * Drag and drop of drawings onto canvas.
+ */
+
+/* Authors:
+ *
+ * Copyright (C) Tavmjong Bah 2019
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "drag-and-drop.h"
+
+#include <glibmm/i18n.h> // Internationalization
+
+#include "desktop-style.h"
+#include "document-undo.h"
+#include "gradient-drag.h"
+#include "file.h"
+#include "inkscape.h" // SP_ACTIVE_DESKTOP
+#include "splivarot.h"
+#include "style.h"
+
+#include "display/sp-canvas.h" // window to world transform
+
+#include "extension/db.h"
+#include "extension/find_extension_by_mime.h"
+
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+#include "object/sp-flowtext.h"
+
+#include "svg/svg-color.h" // write color
+
+#include "ui/clipboard.h"
+#include "ui/interface.h"
+#include "ui/tools/tool-base.h"
+
+#include "widgets/ege-paint-def.h"
+
+using Inkscape::DocumentUndo;
+
+/* Drag and Drop */
+enum ui_drop_target_info {
+ URI_LIST,
+ SVG_XML_DATA,
+ SVG_DATA,
+ PNG_DATA,
+ JPEG_DATA,
+ IMAGE_DATA,
+ APP_X_INKY_COLOR,
+ APP_X_COLOR,
+ APP_OSWB_COLOR,
+ APP_X_INK_PASTE
+};
+
+static GtkTargetEntry ui_drop_target_entries [] = {
+ {(gchar *)"text/uri-list", 0, URI_LIST },
+ {(gchar *)"image/svg+xml", 0, SVG_XML_DATA },
+ {(gchar *)"image/svg", 0, SVG_DATA },
+ {(gchar *)"image/png", 0, PNG_DATA },
+ {(gchar *)"image/jpeg", 0, JPEG_DATA },
+#if ENABLE_MAGIC_COLORS
+ {(gchar *)"application/x-inkscape-color", 0, APP_X_INKY_COLOR},
+#endif // ENABLE_MAGIC_COLORS
+ {(gchar *)"application/x-oswb-color", 0, APP_OSWB_COLOR },
+ {(gchar *)"application/x-color", 0, APP_X_COLOR },
+ {(gchar *)"application/x-inkscape-paste", 0, APP_X_INK_PASTE }
+};
+
+static GtkTargetEntry *completeDropTargets = nullptr;
+static int completeDropTargetsCount = 0;
+
+static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries);
+
+/* Drag and Drop */
+void
+ink_drag_data_received(GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint /*event_time*/,
+ gpointer /*user_data*/)
+{
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ switch (info) {
+#if ENABLE_MAGIC_COLORS
+ case APP_X_INKY_COLOR:
+ {
+ int destX = 0;
+ int destY = 0;
+ gtk_widget_translate_coordinates( widget, &(desktop->canvas->widget), x, y, &destX, &destY );
+ Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) );
+
+ SPItem *item = desktop->getItemAtPoint( where, true );
+ if ( item )
+ {
+ bool fillnotstroke = (drag_context->action != GDK_ACTION_MOVE);
+
+ if ( data->length >= 8 ) {
+ cmsHPROFILE srgbProf = cmsCreate_sRGBProfile();
+
+ gchar c[64] = {0};
+ // Careful about endian issues.
+ guint16* dataVals = (guint16*)data->data;
+ sp_svg_write_color( c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ 0x0ff & (dataVals[0] >> 8),
+ 0x0ff & (dataVals[1] >> 8),
+ 0x0ff & (dataVals[2] >> 8),
+ 0xff // can't have transparency in the color itself
+ //0x0ff & (data->data[3] >> 8),
+ ));
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ bool updatePerformed = false;
+
+ if ( data->length > 14 ) {
+ int flags = dataVals[4];
+
+ // piggie-backed palette entry info
+ int index = dataVals[5];
+ Glib::ustring palName;
+ for ( int i = 0; i < dataVals[6]; i++ ) {
+ palName += (gunichar)dataVals[7+i];
+ }
+
+ // Now hook in a magic tag of some sort.
+ if ( !palName.empty() && (flags & 1) ) {
+ gchar* str = g_strdup_printf("%d|", index);
+ palName.insert( 0, str );
+ g_free(str);
+ str = 0;
+
+ item->setAttribute(
+ fillnotstroke ? "inkscape:x-fill-tag":"inkscape:x-stroke-tag",
+ palName.c_str(),
+ false );
+ item->updateRepr();
+
+ sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", c );
+ updatePerformed = true;
+ }
+ }
+
+ if ( !updatePerformed ) {
+ sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", c );
+ }
+
+ sp_desktop_apply_css_recursive( item, css, true );
+ item->updateRepr();
+
+ SPDocumentUndo::done( doc , SP_VERB_NONE,
+ _("Drop color"));
+
+ if ( srgbProf ) {
+ cmsCloseProfile( srgbProf );
+ }
+ }
+ }
+ }
+ break;
+#endif // ENABLE_MAGIC_COLORS
+
+ case APP_X_COLOR:
+ {
+ int destX = 0;
+ int destY = 0;
+ gtk_widget_translate_coordinates( widget, GTK_WIDGET(desktop->canvas), x, y, &destX, &destY );
+ Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) );
+ Geom::Point const button_dt(desktop->w2d(where));
+ Geom::Point const button_doc(desktop->dt2doc(button_dt));
+
+ if ( gtk_selection_data_get_length (data) == 8 ) {
+ gchar colorspec[64] = {0};
+ // Careful about endian issues.
+ guint16* dataVals = (guint16*)gtk_selection_data_get_data (data);
+ sp_svg_write_color( colorspec, sizeof(colorspec),
+ SP_RGBA32_U_COMPOSE(
+ 0x0ff & (dataVals[0] >> 8),
+ 0x0ff & (dataVals[1] >> 8),
+ 0x0ff & (dataVals[2] >> 8),
+ 0xff // can't have transparency in the color itself
+ //0x0ff & (data->data[3] >> 8),
+ ));
+
+ SPItem *item = desktop->getItemAtPoint( where, true );
+
+ bool consumed = false;
+ if (desktop->event_context && desktop->event_context->get_drag()) {
+ consumed = desktop->event_context->get_drag()->dropColor(item, colorspec, button_dt);
+ if (consumed) {
+ DocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient") );
+ desktop->event_context->get_drag()->updateDraggers();
+ }
+ }
+
+ //if (!consumed && tools_active(desktop, TOOLS_TEXT)) {
+ // consumed = sp_text_context_drop_color(c, button_doc);
+ // if (consumed) {
+ // SPDocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient stop"));
+ // }
+ //}
+
+ if (!consumed && item) {
+ bool fillnotstroke = (gdk_drag_context_get_actions (drag_context) != GDK_ACTION_MOVE);
+ if (fillnotstroke &&
+ (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ boost::optional<Path::cut_position> position = get_nearest_position_on_Path(livarot_path, button_doc);
+ if (position) {
+ Geom::Point nearest = get_point_on_Path(livarot_path, position->piece, position->t);
+ Geom::Point delta = nearest - button_doc;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ delta = desktop->d2w(delta);
+ double stroke_tolerance =
+ ( !item->style->stroke.isNone() ?
+ desktop->current_zoom() *
+ item->style->stroke_width.computed *
+ item->i2dt_affine().descrim() * 0.5
+ : 0.0)
+ + prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ if (Geom::L2 (delta) < stroke_tolerance) {
+ fillnotstroke = false;
+ }
+ }
+ delete livarot_path;
+ }
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", colorspec );
+
+ sp_desktop_apply_css_recursive( item, css, true );
+ item->updateRepr();
+
+ DocumentUndo::done( doc , SP_VERB_NONE,
+ _("Drop color") );
+ }
+ }
+ }
+ break;
+
+ case APP_OSWB_COLOR:
+ {
+ bool worked = false;
+ Glib::ustring colorspec;
+ if ( gtk_selection_data_get_format (data) == 8 ) {
+ ege::PaintDef color;
+ worked = color.fromMIMEData("application/x-oswb-color",
+ reinterpret_cast<char const *>(gtk_selection_data_get_data (data)),
+ gtk_selection_data_get_length (data),
+ gtk_selection_data_get_format (data));
+ if ( worked ) {
+ if ( color.getType() == ege::PaintDef::CLEAR ) {
+ colorspec = ""; // TODO check if this is sufficient
+ } else if ( color.getType() == ege::PaintDef::NONE ) {
+ colorspec = "none";
+ } else {
+ unsigned int r = color.getR();
+ unsigned int g = color.getG();
+ unsigned int b = color.getB();
+
+ SPGradient* matches = nullptr;
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( color.descr == grad->getId() ) {
+ if ( grad->hasStops() ) {
+ matches = grad;
+ break;
+ }
+ }
+ }
+ if (matches) {
+ colorspec = "url(#";
+ colorspec += matches->getId();
+ colorspec += ")";
+ } else {
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b);
+ colorspec = tmp;
+ g_free(tmp);
+ }
+ }
+ }
+ }
+ if ( worked ) {
+ int destX = 0;
+ int destY = 0;
+ gtk_widget_translate_coordinates( widget, GTK_WIDGET(desktop->canvas), x, y, &destX, &destY );
+ Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) );
+ Geom::Point const button_dt(desktop->w2d(where));
+ Geom::Point const button_doc(desktop->dt2doc(button_dt));
+
+ SPItem *item = desktop->getItemAtPoint( where, true );
+
+ bool consumed = false;
+ if (desktop->event_context && desktop->event_context->get_drag()) {
+ consumed = desktop->event_context->get_drag()->dropColor(item, colorspec.c_str(), button_dt);
+ if (consumed) {
+ DocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient") );
+ desktop->event_context->get_drag()->updateDraggers();
+ }
+ }
+
+ if (!consumed && item) {
+ bool fillnotstroke = (gdk_drag_context_get_actions (drag_context) != GDK_ACTION_MOVE);
+ if (fillnotstroke &&
+ (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ boost::optional<Path::cut_position> position = get_nearest_position_on_Path(livarot_path, button_doc);
+ if (position) {
+ Geom::Point nearest = get_point_on_Path(livarot_path, position->piece, position->t);
+ Geom::Point delta = nearest - button_doc;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ delta = desktop->d2w(delta);
+ double stroke_tolerance =
+ ( !item->style->stroke.isNone() ?
+ desktop->current_zoom() *
+ item->style->stroke_width.computed *
+ item->i2dt_affine().descrim() * 0.5
+ : 0.0)
+ + prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ if (Geom::L2 (delta) < stroke_tolerance) {
+ fillnotstroke = false;
+ }
+ }
+ delete livarot_path;
+ }
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", colorspec.c_str() );
+
+ sp_desktop_apply_css_recursive( item, css, true );
+ item->updateRepr();
+
+ DocumentUndo::done( doc , SP_VERB_NONE,
+ _("Drop color") );
+ }
+ }
+ }
+ break;
+
+ case SVG_DATA:
+ case SVG_XML_DATA: {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/onimport", true);
+ gchar *svgdata = (gchar *)gtk_selection_data_get_data (data);
+
+ Inkscape::XML::Document *rnewdoc = sp_repr_read_mem(svgdata, gtk_selection_data_get_length (data), SP_SVG_NS_URI);
+
+ if (rnewdoc == nullptr) {
+ sp_ui_error_dialog(_("Could not parse SVG data"));
+ return;
+ }
+
+ Inkscape::XML::Node *repr = rnewdoc->root();
+ gchar const *style = repr->attribute("style");
+
+ Inkscape::XML::Node *newgroup = rnewdoc->createElement("svg:g");
+ newgroup->setAttribute("style", style);
+
+ Inkscape::XML::Document * xml_doc = doc->getReprDoc();
+ for (Inkscape::XML::Node *child = repr->firstChild(); child != nullptr; child = child->next()) {
+ Inkscape::XML::Node *newchild = child->duplicate(xml_doc);
+ newgroup->appendChild(newchild);
+ }
+
+ Inkscape::GC::release(rnewdoc);
+
+ // Add it to the current layer
+
+ // Greg's edits to add intelligent positioning of svg drops
+ SPObject *new_obj = nullptr;
+ new_obj = desktop->currentLayer()->appendChildRepr(newgroup);
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->set(SP_ITEM(new_obj));
+
+ // move to mouse pointer
+ {
+ desktop->getDocument()->ensureUpToDate();
+ Geom::OptRect sel_bbox = selection->visualBounds();
+ if (sel_bbox) {
+ Geom::Point m( desktop->point() - sel_bbox->midpoint() );
+ selection->moveRelative(m, false);
+ }
+ }
+
+ Inkscape::GC::release(newgroup);
+ DocumentUndo::done( doc, SP_VERB_NONE,
+ _("Drop SVG") );
+ prefs->setBool("/options/onimport", false);
+ break;
+ }
+
+ case URI_LIST: {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/onimport", true);
+ gchar *uri = (gchar *)gtk_selection_data_get_data (data);
+ sp_ui_import_files(uri);
+ prefs->setBool("/options/onimport", false);
+ break;
+ }
+
+ case APP_X_INK_PASTE: {
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ cm->paste(desktop);
+ DocumentUndo::done( doc, SP_VERB_NONE, _("Drop Symbol") );
+ break;
+ }
+
+ case PNG_DATA:
+ case JPEG_DATA:
+ case IMAGE_DATA: {
+ Inkscape::Extension::Extension *ext = Inkscape::Extension::find_by_mime((info == JPEG_DATA ? "image/jpeg" : "image/png"));
+ bool save = (strcmp(ext->get_param_optiongroup("link"), "embed") == 0);
+ ext->set_param_optiongroup("link", "embed");
+ ext->set_gui(false);
+
+ gchar *filename = g_build_filename( g_get_tmp_dir(), "inkscape-dnd-import", NULL );
+ g_file_set_contents(filename,
+ reinterpret_cast<gchar const *>(gtk_selection_data_get_data (data)),
+ gtk_selection_data_get_length (data),
+ nullptr);
+ file_import(doc, filename, ext);
+ g_free(filename);
+
+ ext->set_param_optiongroup("link", save ? "embed" : "link");
+ ext->set_gui(true);
+ DocumentUndo::done( doc , SP_VERB_NONE,
+ _("Drop bitmap image") );
+ break;
+ }
+ }
+}
+
+#include "ui/tools/gradient-tool.h"
+
+void ink_drag_motion( GtkWidget */*widget*/,
+ GdkDragContext */*drag_context*/,
+ gint /*x*/, gint /*y*/,
+ GtkSelectionData */*data*/,
+ guint /*info*/,
+ guint /*event_time*/,
+ gpointer /*user_data*/)
+{
+// SPDocument *doc = SP_ACTIVE_DOCUMENT;
+// SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+
+// g_message("drag-n-drop motion (%4d, %4d) at %d", x, y, event_time);
+}
+
+static void ink_drag_leave( GtkWidget */*widget*/,
+ GdkDragContext */*drag_context*/,
+ guint /*event_time*/,
+ gpointer /*user_data*/ )
+{
+// g_message("drag-n-drop leave at %d", event_time);
+}
+
+void
+ink_drag_setup(Gtk::Widget* win)
+{
+ if ( completeDropTargets == nullptr || completeDropTargetsCount == 0 )
+ {
+ std::vector<Glib::ustring> types;
+
+ std::vector<Gdk::PixbufFormat> list = Gdk::Pixbuf::get_formats();
+ for (auto one:list) {
+ std::vector<Glib::ustring> typesXX = one.get_mime_types();
+ for (auto i:typesXX) {
+ types.push_back(i);
+ }
+ }
+ completeDropTargetsCount = nui_drop_target_entries + types.size();
+ completeDropTargets = new GtkTargetEntry[completeDropTargetsCount];
+ for ( int i = 0; i < (int)nui_drop_target_entries; i++ ) {
+ completeDropTargets[i] = ui_drop_target_entries[i];
+ }
+ int pos = nui_drop_target_entries;
+
+ for (auto & type : types) {
+ completeDropTargets[pos].target = g_strdup(type.c_str());
+ completeDropTargets[pos].flags = 0;
+ completeDropTargets[pos].info = IMAGE_DATA;
+ pos++;
+ }
+ }
+
+ gtk_drag_dest_set((GtkWidget*)win->gobj(),
+ GTK_DEST_DEFAULT_ALL,
+ completeDropTargets,
+ completeDropTargetsCount,
+ GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE));
+
+ g_signal_connect(G_OBJECT(win->gobj()),
+ "drag_data_received",
+ G_CALLBACK(ink_drag_data_received),
+ NULL);
+
+ g_signal_connect(G_OBJECT(win->gobj()),
+ "drag_motion",
+ G_CALLBACK(ink_drag_motion),
+ NULL);
+
+ g_signal_connect(G_OBJECT(win->gobj()),
+ "drag_leave",
+ G_CALLBACK(ink_drag_leave),
+ NULL);
+}
+
+
+/*
+ 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/drag-and-drop.h b/src/ui/drag-and-drop.h
new file mode 100644
index 0000000..fb3a445
--- /dev/null
+++ b/src/ui/drag-and-drop.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_DRAG_AND_DROP_H
+#define SEEN_CANVAS_DRAG_AND_DROP_H
+
+/**
+ * @file
+ * Drag and drop of drawings onto canvas.
+ */
+
+/* Authors:
+ *
+ * Copyright (C) Tavmjong Bah 2019
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+void ink_drag_setup(Gtk::Widget* win);
+
+static void ink_drag_data_received(GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint event_time,
+ gpointer user_data);
+static void ink_drag_motion( GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint event_time,
+ gpointer user_data );
+static void ink_drag_leave( GtkWidget *widget,
+ GdkDragContext *drag_context,
+ guint event_time,
+ gpointer user_data );
+
+#endif // SEEN_CANVAS_DRAG_AND_DROP_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/draw-anchor.cpp b/src/ui/draw-anchor.cpp
new file mode 100644
index 0000000..1b32525
--- /dev/null
+++ b/src/ui/draw-anchor.cpp
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Anchors implementation.
+ */
+
+/*
+ * Authors:
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2004 Monash University
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "ui/draw-anchor.h"
+#include "desktop.h"
+#include "ui/tools/tool-base.h"
+#include "ui/tools/lpe-tool.h"
+#include "display/sodipodi-ctrl.h"
+#include "display/curve.h"
+#include "ui/control-manager.h"
+
+using Inkscape::ControlManager;
+
+#define FILL_COLOR_NORMAL 0xffffff7f
+#define FILL_COLOR_MOUSEOVER 0xff0000ff
+
+/**
+ * Creates an anchor object and initializes it.
+ */
+SPDrawAnchor *sp_draw_anchor_new(Inkscape::UI::Tools::FreehandBase *dc, SPCurve *curve, bool start, Geom::Point delta)
+{
+ if (SP_IS_LPETOOL_CONTEXT(dc)) {
+ // suppress all kinds of anchors in LPEToolContext
+ return nullptr;
+ }
+
+ SPDrawAnchor *a = g_new(SPDrawAnchor, 1);
+
+ a->dc = dc;
+ a->curve = curve;
+ curve->ref();
+ a->start = start;
+ a->active = FALSE;
+ a->dp = delta;
+ a->ctrl = ControlManager::getManager().createControl(dc->getDesktop().getControls(), Inkscape::CTRL_TYPE_ANCHOR);
+
+ SP_CTRL(a->ctrl)->moveto(delta);
+
+ ControlManager::getManager().track(a->ctrl);
+
+ return a;
+}
+
+/**
+ * Destroys the anchor's canvas item and frees the anchor object.
+ */
+SPDrawAnchor *sp_draw_anchor_destroy(SPDrawAnchor *anchor)
+{
+ if (anchor->curve) {
+ anchor->curve->unref();
+ }
+ if (anchor->ctrl) {
+ sp_canvas_item_destroy(anchor->ctrl);
+ }
+ g_free(anchor);
+ return nullptr;
+}
+
+/**
+ * Test if point is near anchor, if so fill anchor on canvas and return
+ * pointer to it or NULL.
+ */
+SPDrawAnchor *sp_draw_anchor_test(SPDrawAnchor *anchor, Geom::Point w, bool activate)
+{
+ SPCtrl *ctrl = SP_CTRL(anchor->ctrl);
+
+ if ( activate && ( Geom::LInfty( w - anchor->dc->getDesktop().d2w(anchor->dp) ) <= (ctrl->box.width() / 2.0) ) ) {
+ if (!anchor->active) {
+ ControlManager::getManager().setControlResize(anchor->ctrl, 4);
+ g_object_set(anchor->ctrl, "fill_color", FILL_COLOR_MOUSEOVER, NULL);
+ anchor->active = TRUE;
+ }
+ return anchor;
+ }
+
+ if (anchor->active) {
+ ControlManager::getManager().setControlResize(anchor->ctrl, 0);
+ g_object_set(anchor->ctrl, "fill_color", FILL_COLOR_NORMAL, NULL);
+ anchor->active = FALSE;
+ }
+
+ return nullptr;
+}
+
+
+/*
+ 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/draw-anchor.h b/src/ui/draw-anchor.h
new file mode 100644
index 0000000..b631a48
--- /dev/null
+++ b/src/ui/draw-anchor.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_DRAW_ANCHOR_H
+#define SEEN_DRAW_ANCHOR_H
+
+/** \file
+ * Drawing anchors.
+ */
+
+#include <2geom/point.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class FreehandBase;
+
+}
+}
+}
+
+class SPCurve;
+struct SPCanvasItem;
+
+/// The drawing anchor.
+/// \todo Make this a regular knot, this will allow setting statusbar tips.
+struct SPDrawAnchor {
+ Inkscape::UI::Tools::FreehandBase *dc;
+ SPCurve *curve;
+ unsigned int start : 1;
+ unsigned int active : 1;
+ Geom::Point dp;
+ SPCanvasItem *ctrl;
+};
+
+
+SPDrawAnchor *sp_draw_anchor_new(Inkscape::UI::Tools::FreehandBase *dc, SPCurve *curve, bool start,
+ Geom::Point delta);
+SPDrawAnchor *sp_draw_anchor_destroy(SPDrawAnchor *anchor);
+SPDrawAnchor *sp_draw_anchor_test(SPDrawAnchor *anchor, Geom::Point w, bool activate);
+
+
+#endif /* !SEEN_DRAW_ANCHOR_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/event-debug.h b/src/ui/event-debug.h
new file mode 100644
index 0000000..150b847
--- /dev/null
+++ b/src/ui/event-debug.h
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_EVENT_DEBUG_H
+#define SEEN_UI_EVENT_DEBUG_H
+
+/**
+ * @file
+ * Dump event data.
+ */
+/*
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtk/gtk.h>
+#include <glibmm/ustring.h>
+
+// See: https://developer.gnome.org/gdk3/stable/gdk3-Events.html
+
+inline void ui_dump_event (GdkEvent *event, Glib::ustring const &prefix, bool merge = true) {
+
+ static GdkEventType old_type = GDK_NOTHING;
+ static unsigned count = 0;
+
+ // Doesn't usually help to dump a zillion events of the same type (e.g. GDK_MOTION_NOTIFY).
+ ++count;
+ if (merge && event->type == old_type) {
+ if ( count == 1 ) {
+ std::cout << prefix << " ... ditto" << std::endl;
+ }
+ return;
+ }
+ count = 0;
+ old_type = event->type;
+
+ std::cout << prefix << ": ";
+
+ switch (event->type) {
+
+ case GDK_KEY_PRESS:
+ std::cout << "GDK_KEY_PRESS: " << event->key.hardware_keycode << std::endl;
+ break;
+ case GDK_KEY_RELEASE:
+ std::cout << "GDK_KEY_RELEASE: " << event->key.hardware_keycode << std::endl;
+ break;
+
+ case GDK_BUTTON_PRESS:
+ std::cout << "GDK_BUTTON_PRESS: " << event->button.button << std::endl;
+ break;
+ case GDK_2BUTTON_PRESS:
+ std::cout << "GDK_2BUTTON_PRESS: " << event->button.button << std::endl;
+ break;
+ case GDK_3BUTTON_PRESS:
+ std::cout << "GDK_3BUTTON_PRESS: " << event->button.button << std::endl;
+ break;
+ case GDK_BUTTON_RELEASE:
+ std::cout << "GDK_BUTTON_RELEASE: " << event->button.button << std::endl;
+ break;
+
+ case GDK_SCROLL:
+ std::cout << "GDK_SCROLL" << std::endl;
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ std::cout << "GDK_MOTION_NOTIFY" << std::endl;
+ break;
+ case GDK_ENTER_NOTIFY:
+ std::cout << "GDK_ENTER_NOTIFY" << std::endl;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ std::cout << "GDK_LEAVE_NOTIFY" << std::endl;
+ break;
+
+ case GDK_TOUCH_BEGIN:
+ std::cout << "GDK_TOUCH_BEGIN" << std::endl;
+ break;
+ case GDK_TOUCH_UPDATE:
+ std::cout << "GDK_TOUCH_UPDATE" << std::endl;
+ break;
+ case GDK_TOUCH_END:
+ std::cout << "GDK_TOUCH_END" << std::endl;
+ break;
+ case GDK_TOUCH_CANCEL:
+ std::cout << "GDK_TOUCH_CANCEL" << std::endl;
+ break;
+ case GDK_TOUCHPAD_SWIPE:
+ std::cout << "GDK_TOUCHPAD_SWIPE" << std::endl;
+ break;
+ case GDK_TOUCHPAD_PINCH:
+ std::cout << "GDK_TOUCHPAD_PINCH" << std::endl;
+ break;
+ case GDK_PAD_BUTTON_PRESS:
+ std::cout << "GDK_PAD_BUTTON_PRESS" << std::endl;
+ break;
+ case GDK_PAD_BUTTON_RELEASE:
+ std::cout << "GDK_PAD_BUTTON_RELEASE" << std::endl;
+ break;
+ case GDK_PAD_RING:
+ std::cout << "GDK_PAD_RING" << std::endl;
+ break;
+ case GDK_PAD_STRIP:
+ std::cout << "GDK_PAD_STRIP" << std::endl;
+ break;
+ case GDK_PAD_GROUP_MODE:
+ std::cout << "GDK_PAD_GROUP_MODE" << std::endl;
+ break;
+ default:
+ std::cout << "GDK event not recognized!" << std::endl;
+ break;
+ }
+}
+
+#endif // SEEN_UI_EVENT_DEBUG_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/icon-loader.cpp b/src/ui/icon-loader.cpp
new file mode 100644
index 0000000..62159c2
--- /dev/null
+++ b/src/ui/icon-loader.cpp
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Icon Loader
+ *
+ * Icon Loader management code
+ *
+ * Authors:
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "icon-loader.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "svg/svg-color.h"
+#include "widgets/toolbox.h"
+#include <fstream>
+#include <gdkmm/display.h>
+#include <gdkmm/screen.h>
+#include <gtkmm/iconinfo.h>
+#include <gtkmm/icontheme.h>
+
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gint size)
+{
+ Gtk::Image *icon = new Gtk::Image();
+ icon->set_from_icon_name(icon_name, Gtk::IconSize(Gtk::ICON_SIZE_BUTTON));
+ icon->set_pixel_size(size);
+ return icon;
+}
+
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::IconSize icon_size)
+{
+ Gtk::Image *icon = new Gtk::Image();
+ icon->set_from_icon_name(icon_name, icon_size);
+ return icon;
+}
+
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size)
+{
+ Gtk::Image *icon = new Gtk::Image();
+ icon->set_from_icon_name(icon_name, icon_size);
+ return icon;
+}
+
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gchar const *prefs_size)
+{
+ Gtk::IconSize icon_size = Inkscape::UI::ToolboxFactory::prefToSize_mm(prefs_size);
+ return sp_get_icon_image(icon_name, icon_size);
+}
+
+GtkWidget *sp_get_icon_image(Glib::ustring icon_name, GtkIconSize icon_size)
+{
+ return gtk_image_new_from_icon_name(icon_name.c_str(), icon_size);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, gint size)
+{
+ Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
+ Glib::RefPtr<Gdk::Screen> screen = display->get_default_screen();
+ Glib::RefPtr<Gtk::IconTheme> icon_theme = Gtk::IconTheme::get_for_screen(screen);
+ Glib::RefPtr<Gdk::Pixbuf> _icon_pixbuf;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name + Glib::ustring("-symbolic"), size, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ if (iconinfo && SP_ACTIVE_DESKTOP->getToplevel()) {
+ bool was_symbolic = false;
+ Glib::ustring css_str = "";
+ 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);
+ gchar colornamed[64];
+ gchar colornamedsuccess[64];
+ gchar colornamedwarning[64];
+ gchar colornamederror[64];
+ sp_svg_write_color(colornamed, sizeof(colornamed), colorsetbase);
+ sp_svg_write_color(colornamedsuccess, sizeof(colornamedsuccess), colorsetsuccess);
+ sp_svg_write_color(colornamedwarning, sizeof(colornamedwarning), colorsetwarning);
+ sp_svg_write_color(colornamederror, sizeof(colornamederror), colorseterror);
+ _icon_pixbuf =
+ iconinfo.load_symbolic(Gdk::RGBA(colornamed), Gdk::RGBA(colornamedsuccess),
+ Gdk::RGBA(colornamedwarning), Gdk::RGBA(colornamederror), was_symbolic);
+ } else {
+ Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ _icon_pixbuf = iconinfo.load_icon();
+ }
+ } else {
+ Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ _icon_pixbuf = iconinfo.load_icon();
+ }
+ return _icon_pixbuf;
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::IconSize icon_size)
+{
+ int width, height;
+ Gtk::IconSize::lookup(icon_size, width, height);
+ return sp_get_icon_pixbuf(icon_name, width);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size)
+{
+ int width, height;
+ Gtk::IconSize::lookup(Gtk::IconSize(icon_size), width, height);
+ return sp_get_icon_pixbuf(icon_name, width);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size)
+{
+ gint width, height;
+ gtk_icon_size_lookup(icon_size, &width, &height);
+ return sp_get_icon_pixbuf(icon_name, width);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, gchar const *prefs_size)
+{
+ // Load icon based in preference size defined allowed values are:
+ //"/toolbox/tools/small" Toolbox icon size
+ //"/toolbox/small" Control bar icon size
+ //"/toolbox/secondary" Secondary toolbar icon size
+ GtkIconSize icon_size = Inkscape::UI::ToolboxFactory::prefToSize(prefs_size);
+ return sp_get_icon_pixbuf(icon_name, icon_size);
+}
+
+/*
+ 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/icon-loader.h b/src/ui/icon-loader.h
new file mode 100644
index 0000000..78975e2
--- /dev/null
+++ b/src/ui/icon-loader.h
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Icon Loader
+ *//*
+ * Authors:
+ * see git history
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_INK_ICON_LOADER_H
+#define SEEN_INK_ICON_LOADER_H
+
+#include <gdkmm/pixbuf.h>
+#include <gtkmm/image.h>
+
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gint size);
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size);
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::IconSize icon_size);
+Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gchar const *prefs_sice);
+GtkWidget *sp_get_icon_image(Glib::ustring icon_name, GtkIconSize icon_size);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, gint size);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::IconSize icon_size);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, gchar const *prefs_sice);
+
+#endif // SEEN_INK_ICON_LOADER_H
diff --git a/src/ui/icon-names.h b/src/ui/icon-names.h
new file mode 100644
index 0000000..2ec0dce
--- /dev/null
+++ b/src/ui/icon-names.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Macro for icon names used in Inkscape
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_ICON_NAMES_H
+#define SEEN_INKSCAPE_ICON_NAMES_H
+
+/** @brief Icon name annotation.
+ * Use this macro to mark strings which are used as icon names.
+ * This greatly simplifies tasks such as obtaining a full list of icons
+ * used by Inkscape. */
+#define INKSCAPE_ICON(icon) icon
+
+#endif /* ifdef SEEN_INKSCAPE_ICON_NAMES_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/interface.cpp b/src/ui/interface.cpp
new file mode 100644
index 0000000..dba6051
--- /dev/null
+++ b/src/ui/interface.cpp
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Main UI stuff.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 "desktop.h"
+#include "document.h"
+#include "enums.h"
+#include "file.h"
+#include "inkscape.h"
+#include "inkscape-window.h"
+#include "preferences.h"
+#include "shortcuts.h"
+
+#include "extension/db.h"
+#include "extension/effect.h"
+#include "extension/find_extension_by_mime.h"
+#include "extension/input.h"
+
+#include "helper/action.h"
+
+#include "io/sys.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+
+#include "ui/dialog-events.h"
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/inkscape-preferences.h"
+#include "ui/dialog/layer-properties.h"
+#include "ui/interface.h"
+
+#include "ui/view/svg-view-widget.h"
+
+#include "widgets/desktop-widget.h"
+
+static void sp_ui_import_one_file(char const *filename);
+static void sp_ui_import_one_file_with_check(gpointer filename, gpointer unused);
+
+void
+sp_ui_new_view()
+{
+ SPDocument *document;
+
+ document = SP_ACTIVE_DOCUMENT;
+ if (!document) return;
+
+ ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::get_instance());
+
+ InkscapeWindow* win = app->window_open (document);
+}
+
+void
+sp_ui_close_view(GtkWidget */*widget*/)
+{
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+
+ if (dt == nullptr) {
+ return;
+ }
+
+ if (dt->shutdown()) {
+ return; // Shutdown operation has been canceled, so do nothing
+ }
+
+ ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::get_instance());
+
+ InkscapeWindow* window = SP_ACTIVE_DESKTOP->getInkscapeWindow();
+
+ // If closing the last document, open a new document so Inkscape doesn't quit.
+ std::list<SPDesktop *> desktops;
+ INKSCAPE.get_all_desktops(desktops);
+ if (desktops.size() == 1) {
+
+ SPDocument* old_document = window->get_document();
+
+ Glib::ustring template_path = sp_file_default_template_uri();
+ SPDocument *doc = app->document_new (template_path);
+
+ app->document_swap (window, doc);
+
+ if (app->document_window_count(old_document) == 0) {
+ app->document_close(old_document);
+ }
+
+ // Are these necessary?
+ sp_namedview_window_from_document(dt);
+ sp_namedview_update_layers_from_document(dt);
+
+ } else {
+
+ app->destroy_window (window);
+ }
+}
+
+
+unsigned int
+sp_ui_close_all()
+{
+
+ ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::get_instance());
+
+ app->destroy_all();
+
+ return true;
+}
+
+
+void
+sp_ui_dialog_title_string(Inkscape::Verb *verb, gchar *c)
+{
+ SPAction *action;
+ unsigned int shortcut;
+ gchar *s;
+ gchar *atitle;
+
+ action = verb->get_action(Inkscape::ActionContext());
+ if (!action)
+ return;
+
+ atitle = sp_action_get_title(action);
+
+ s = g_stpcpy(c, atitle);
+
+ g_free(atitle);
+
+ shortcut = sp_shortcut_get_primary(verb);
+ if (shortcut!=GDK_KEY_VoidSymbol) {
+ gchar* key = sp_shortcut_get_label(shortcut);
+ s = g_stpcpy(s, " (");
+ s = g_stpcpy(s, key);
+ g_stpcpy(s, ")");
+ g_free(key);
+ }
+}
+
+
+Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view )
+{
+ Glib::ustring prefPath;
+
+ if (reinterpret_cast<SPDesktop*>(view)->is_focusMode()) {
+ prefPath = "/focus/";
+ } else if (reinterpret_cast<SPDesktop*>(view)->is_fullscreen()) {
+ prefPath = "/fullscreen/";
+ } else {
+ prefPath = "/window/";
+ }
+
+ return prefPath;
+}
+
+
+void
+sp_ui_import_files(gchar *buffer)
+{
+ gchar** l = g_uri_list_extract_uris(buffer);
+ for (unsigned int i=0; i < g_strv_length(l); i++) {
+ gchar *f = g_filename_from_uri (l[i], nullptr, nullptr);
+ sp_ui_import_one_file_with_check(f, nullptr);
+ g_free(f);
+ }
+ g_strfreev(l);
+}
+
+static void
+sp_ui_import_one_file_with_check(gpointer filename, gpointer /*unused*/)
+{
+ if (filename) {
+ if (strlen((char const *)filename) > 2)
+ sp_ui_import_one_file((char const *)filename);
+ }
+}
+
+static void
+sp_ui_import_one_file(char const *filename)
+{
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ if (!doc) return;
+
+ if (filename == nullptr) return;
+
+ // Pass off to common implementation
+ // TODO might need to get the proper type of Inkscape::Extension::Extension
+ file_import( doc, filename, nullptr );
+}
+
+void
+sp_ui_error_dialog(gchar const *message)
+{
+ GtkWidget *dlg;
+ gchar *safeMsg = Inkscape::IO::sanitizeString(message);
+
+ dlg = gtk_message_dialog_new(nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE, "%s", safeMsg);
+ sp_transientize(dlg);
+ gtk_window_set_resizable(GTK_WINDOW(dlg), FALSE);
+ gtk_dialog_run(GTK_DIALOG(dlg));
+ gtk_widget_destroy(dlg);
+ g_free(safeMsg);
+}
+
+bool
+sp_ui_overwrite_file(gchar const *filename)
+{
+ bool return_value = FALSE;
+
+ if (Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) {
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ gchar* baseName = g_path_get_basename( filename );
+ gchar* dirName = g_path_get_dirname( filename );
+ GtkWidget* dialog = gtk_message_dialog_new_with_markup( window->gobj(),
+ (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_NONE,
+ _( "<span weight=\"bold\" size=\"larger\">A file named \"%s\" already exists. Do you want to replace it?</span>\n\n"
+ "The file already exists in \"%s\". Replacing it will overwrite its contents." ),
+ baseName,
+ dirName
+ );
+ gtk_dialog_add_buttons( GTK_DIALOG(dialog),
+ _("_Cancel"), GTK_RESPONSE_NO,
+ _("Replace"), GTK_RESPONSE_YES,
+ NULL );
+ gtk_dialog_set_default_response( GTK_DIALOG(dialog), GTK_RESPONSE_YES );
+
+ if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_YES ) {
+ return_value = TRUE;
+ } else {
+ return_value = FALSE;
+ }
+ gtk_widget_destroy(dialog);
+ g_free( baseName );
+ g_free( dirName );
+ } else {
+ return_value = TRUE;
+ }
+
+ return return_value;
+}
+
+/*
+ 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/interface.h b/src/ui/interface.h
new file mode 100644
index 0000000..e7e191a
--- /dev/null
+++ b/src/ui/interface.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_INTERFACE_H
+#define SEEN_SP_INTERFACE_H
+
+/*
+ * Main UI stuff
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * 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 <glibmm/ustring.h>
+
+typedef struct _GtkWidget GtkWidget;
+
+namespace Inkscape {
+class Verb;
+
+namespace UI {
+namespace View {
+class View;
+} // namespace View
+} // namespace UI
+} // namespace Inkscape
+
+/**
+ * \param widget unused
+ */
+void sp_ui_close_view (GtkWidget *widget);
+
+void sp_ui_new_view ();
+
+void sp_ui_import_files(gchar *buffer);
+
+/**
+ * This function is called to exit the program, and iterates through all
+ * open document view windows, attempting to close each in turn. If the
+ * view has unsaved information, the user will be prompted to save,
+ * discard, or cancel.
+ *
+ * Returns FALSE if the user cancels the close_all operation, TRUE
+ * otherwise.
+ */
+unsigned int sp_ui_close_all ();
+
+void sp_ui_dialog_title_string (Inkscape::Verb * verb, char* c);
+
+Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view );
+
+/**
+ *
+ */
+void sp_ui_error_dialog (char const* message);
+bool sp_ui_overwrite_file (char const* filename);
+
+#endif // SEEN_SP_INTERFACE_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/monitor.cpp b/src/ui/monitor.cpp
new file mode 100644
index 0000000..a9a3f83
--- /dev/null
+++ b/src/ui/monitor.cpp
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * \brief helper functions for retrieving monitor geometry, etc.
+ *//*
+ * Authors:
+ * see git history
+ * Patrick Storz <eduard.braun2@gmx.de>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/monitor.h>
+#include <gdkmm/rectangle.h>
+#include <gdkmm/window.h>
+
+#include "include/gtkmm_version.h"
+
+namespace Inkscape {
+namespace UI {
+
+/** get monitor geometry of primary monitor */
+Gdk::Rectangle get_monitor_geometry_primary() {
+ Gdk::Rectangle monitor_geometry;
+ auto const display = Gdk::Display::get_default();
+ auto monitor = display->get_primary_monitor();
+
+ // Fallback to monitor number 0 if the user hasn't configured a primary monitor
+ if (!monitor) {
+ monitor = display->get_monitor(0);
+ }
+
+ monitor->get_geometry(monitor_geometry);
+ return monitor_geometry;
+}
+
+/** get monitor geometry of monitor containing largest part of window */
+Gdk::Rectangle get_monitor_geometry_at_window(const Glib::RefPtr<Gdk::Window>& window) {
+ Gdk::Rectangle monitor_geometry;
+ auto const display = Gdk::Display::get_default();
+ auto const monitor = display->get_monitor_at_window(window);
+ monitor->get_geometry(monitor_geometry);
+ return monitor_geometry;
+}
+
+/** get monitor geometry of monitor at (or closest to) point on combined screen area */
+Gdk::Rectangle get_monitor_geometry_at_point(int x, int y) {
+ Gdk::Rectangle monitor_geometry;
+ auto const display = Gdk::Display::get_default();
+ auto const monitor = display->get_monitor_at_point(x ,y);
+ monitor->get_geometry(monitor_geometry);
+ return monitor_geometry;
+}
+
+}
+}
+
+
+/*
+ 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/monitor.h b/src/ui/monitor.h
new file mode 100644
index 0000000..78c105a
--- /dev/null
+++ b/src/ui/monitor.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * \brief helper functions for retrieving monitor geometry, etc.
+ *//*
+ * Authors:
+ * see git history
+ * Patrick Storz <eduard.braun2@gmx.de>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_MONITOR_H
+#define SEEN_MONITOR_H
+
+#include <gdkmm/rectangle.h>
+#include <gdkmm/window.h>
+
+namespace Inkscape {
+namespace UI {
+ Gdk::Rectangle get_monitor_geometry_primary();
+ Gdk::Rectangle get_monitor_geometry_at_window(const Glib::RefPtr<Gdk::Window>& window);
+ Gdk::Rectangle get_monitor_geometry_at_point(int x, int y);
+}
+}
+
+#endif // SEEN_MONITOR_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/pixmaps/README b/src/ui/pixmaps/README
new file mode 100644
index 0000000..0b78941
--- /dev/null
+++ b/src/ui/pixmaps/README
@@ -0,0 +1,2 @@
+
+This directory contains cursor xpm's and handles for resizing/skewing/rotating.
diff --git a/src/ui/pixmaps/cursor-3dbox.xpm b/src/ui/pixmaps/cursor-3dbox.xpm
new file mode 100644
index 0000000..c7fcb92
--- /dev/null
+++ b/src/ui/pixmaps/cursor-3dbox.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_3dbox_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... . ",
+" .+. ..+.. ",
+" .+. ..++.++.. ",
+" ... ..++.....++.. ",
+" .++.........++. ",
+" .+.............+. ",
+" .++...........++. ",
+" .+.++.......++.+. ",
+" .+...+.....+...+. ",
+" .+....++.++....+. ",
+" .+......+......+. ",
+" .+.....+.....+. ",
+" .+.....+.....+. ",
+" .+.....+.....+. ",
+" .+.....+.....+. ",
+" .++...+...++. ",
+" ..+..+..+.. ",
+" .+++++. ",
+" ..+.. ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-adj-a.xpm b/src/ui/pixmaps/cursor-adj-a.xpm
new file mode 100644
index 0000000..7af3d9c
--- /dev/null
+++ b/src/ui/pixmaps/cursor-adj-a.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_adj_a_xpm[] = {
+"32 32 3 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" .. ",
+" .++. ",
+" .++. ",
+" .+..+. ",
+" .+..+. ",
+" .++++. ",
+" .+ .+. ",
+" .... .+..+. ",
+" .++. . ...... ",
+" .++. .+. ",
+" ....++.... .+. ",
+" .++++++++. .+. ",
+" .++++++++. .+. ",
+" ....++.... .+. ",
+" .++. .+. ",
+" .++. .+. ",
+" .... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. .......... ",
+" .+. .++++++++. ",
+" .+. .++++++++. ",
+" .+. .......... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" . ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-adj-h.xpm b/src/ui/pixmaps/cursor-adj-h.xpm
new file mode 100644
index 0000000..5861934
--- /dev/null
+++ b/src/ui/pixmaps/cursor-adj-h.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_adj_h_xpm[] = {
+"32 32 3 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ...... ",
+" .+..+. ",
+" .+..+. ",
+" .+..+. ",
+" .++++. ",
+" .+..+. ",
+" .+ .+. ",
+" .... .+..+. ",
+" .++. . ...... ",
+" .++. .+. ",
+" ....++.... .+. ",
+" .++++++++. .+. ",
+" .++++++++. .+. ",
+" ....++.... .+. ",
+" .++. .+. ",
+" .++. .+. ",
+" .... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. .......... ",
+" .+. .++++++++. ",
+" .+. .++++++++. ",
+" .+. .......... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" . ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-adj-l.xpm b/src/ui/pixmaps/cursor-adj-l.xpm
new file mode 100644
index 0000000..817ce44
--- /dev/null
+++ b/src/ui/pixmaps/cursor-adj-l.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_adj_l_xpm[] = {
+"32 32 3 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+.... ",
+" .... .++++. ",
+" .++. . ...... ",
+" .++. .+. ",
+" ....++.... .+. ",
+" .++++++++. .+. ",
+" .++++++++. .+. ",
+" ....++.... .+. ",
+" .++. .+. ",
+" .++. .+. ",
+" .... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. .......... ",
+" .+. .++++++++. ",
+" .+. .++++++++. ",
+" .+. .......... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" . ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-adj-s.xpm b/src/ui/pixmaps/cursor-adj-s.xpm
new file mode 100644
index 0000000..351a570
--- /dev/null
+++ b/src/ui/pixmaps/cursor-adj-s.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_adj_s_xpm[] = {
+"32 32 3 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" .. ",
+" .++. ",
+" .+..+. ",
+" .+..+. ",
+" .+... ",
+" .+. ",
+" .. .+. ",
+" .... .+..+. ",
+" .++. .. .++. ",
+" .++. .+. .. ",
+" ....++.... .+. ",
+" .++++++++. .+. ",
+" .++++++++. .+. ",
+" ....++.... .+. ",
+" .++. .+. ",
+" .++. .+. ",
+" .... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. .......... ",
+" .+. .++++++++. ",
+" .+. .++++++++. ",
+" .+. .......... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" . ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-calligraphy.xpm b/src/ui/pixmaps/cursor-calligraphy.xpm
new file mode 100644
index 0000000..193bd61
--- /dev/null
+++ b/src/ui/pixmaps/cursor-calligraphy.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_calligraphy_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ... ",
+" .+. ..+++. ",
+" ... .++++++. . ",
+" .++..++++. .+. ",
+" .+. .++. .+. ",
+" .++. .+. .+. ",
+" .+. . .++. ",
+" .+. .+. ",
+" .++. .++. ",
+" .+++.. ...++. ",
+" .++++.++++. ",
+" .++++++++. ",
+" ..++++.. ",
+" .... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-connector.xpm b/src/ui/pixmaps/cursor-connector.xpm
new file mode 100644
index 0000000..86e8d17
--- /dev/null
+++ b/src/ui/pixmaps/cursor-connector.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_connector_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ... ",
+"....+.... .+. ",
+" .+. .+. ",
+" .+. .+. ",
+" ... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ...........+. ",
+" .+++++++++++. ",
+" .+........... ",
+" .+. ",
+" .+. ",
+" ...+... ",
+" .+++++. ",
+" .+++. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-crosshairs.xpm b/src/ui/pixmaps/cursor-crosshairs.xpm
new file mode 100644
index 0000000..407b760
--- /dev/null
+++ b/src/ui/pixmaps/cursor-crosshairs.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_crosshairs_xpm[] = {
+"32 32 3 1 7 7",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" . ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ..... ..... ",
+".+++++ +++++. ",
+" ..... ..... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-dropper-f.xpm b/src/ui/pixmaps/cursor-dropper-f.xpm
new file mode 100644
index 0000000..2804f66
--- /dev/null
+++ b/src/ui/pixmaps/cursor-dropper-f.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static const char * cursor_dropper_f_xpm[] = {
+"32 32 4 1 5 5",
+" c None",
+"@ c Fill",
+". c #FFFFFF",
+"+ c #000000",
+" ... ............",
+" .+. .++++++++++.",
+" .+. .+@@@@@@@@+.",
+" .+. .+@@@@@@@@+.",
+".... .... .+@@@@@@@@+.",
+".+++ +++. .+@@@@@@@@+.",
+".... .... .+@@@@@@@@+.",
+" .+. .+@@@@@@@@+.",
+" .+. .... .+@@@@@@@@+.",
+" .+. .+++. .+@@@@@@@@+.",
+" ... .+..+. .++++++++++.",
+" .++..+. ............",
+" .++..+. ",
+" .++..+. ",
+" .++..+. . ",
+" .++..+.+. ",
+" .++..+++. ",
+" .++++.+. ",
+" .++++.+.. ",
+" .++++++.++. ",
+" .++++++..+. ",
+" ..+++++.+. ",
+" .++++++. ",
+" .+++++. ",
+" .+++. ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-dropper-s.xpm b/src/ui/pixmaps/cursor-dropper-s.xpm
new file mode 100644
index 0000000..c7cb9cc
--- /dev/null
+++ b/src/ui/pixmaps/cursor-dropper-s.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static const char * cursor_dropper_s_xpm[] = {
+"32 32 4 1 5 5",
+" c None",
+"@ s Fill",
+". c #FFFFFF",
+"+ c #000000",
+" ... ...............",
+" .+. .+++++++++++++.",
+" .+. .+@@@@@@@@@@@+.",
+" .+. .+@@@@@@@@@@@+.",
+".... .... .+@@+++++++@@+.",
+".+++ +++. .+@@+.....+@@+.",
+".... .... .+@@+. .+@@+.",
+" .+. .+@@+. .+@@+.",
+" .+. .... .+@@+. .+@@+.",
+" .+. .+++. .+@@+.....+@@+.",
+" ... .+..+. .+@@+++++++@@+.",
+" .++..+. .+@@@@@@@@@@@+.",
+" .++..+. .+@@@@@@@@@@@+.",
+" .++..+..+++++++++++++.",
+" .++..+...............",
+" .++..+.+. ",
+" .++..+++. ",
+" .++++.+. ",
+" .++++.+.. ",
+" .++++++.++. ",
+" .++++++..+. ",
+" ..+++++.+. ",
+" .++++++. ",
+" .+++++. ",
+" .+++. ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-dropping-f.xpm b/src/ui/pixmaps/cursor-dropping-f.xpm
new file mode 100644
index 0000000..8490ba7
--- /dev/null
+++ b/src/ui/pixmaps/cursor-dropping-f.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static const char * cursor_dropping_f_xpm[] = {
+"32 32 4 1 5 5",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+"@ s Fill",
+" ... ............",
+" .+. .++++++++++.",
+" .+. .+@@@@@@@@+.",
+" .+. .+@@@@@@@@+.",
+".... .... .+@@@@@@@@+.",
+".+++ +++. .+@@@@@@@@+.",
+".... .... .+@@@@@@@@+.",
+" .+. ... .+@@@@@@@@+.",
+" .+. .+++. .+@@@@@@@@+.",
+" .+. .+..++. .+@@@@@@@@+.",
+" ... .+.++++. .++++++++++.",
+" ..+.+++++. ............",
+" .++.++++++. ",
+" .++.++++++. ",
+" .++++++.. ",
+" .+.++++. ",
+" .+..++++. ",
+" .+..++.+. ",
+" .+..++. . ",
+" .+++++. ",
+" .+++++. ",
+" .+++++. ",
+" .++++. ",
+" .+++. ",
+" .... ",
+" .+. ",
+" .+++. ",
+".++..+. ",
+".+++.+. ",
+".+++++. ",
+" .+++. ",
+" ... "};
diff --git a/src/ui/pixmaps/cursor-dropping-s.xpm b/src/ui/pixmaps/cursor-dropping-s.xpm
new file mode 100644
index 0000000..3cc8965
--- /dev/null
+++ b/src/ui/pixmaps/cursor-dropping-s.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static const char * cursor_dropping_s_xpm[] = {
+"32 32 4 1 5 5",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+"@ s Fill",
+" ... ...............",
+" .+. .+++++++++++++.",
+" .+. .+@@@@@@@@@@@+.",
+" .+. .+@@@@@@@@@@@+.",
+".... .... .+@@+++++++@@+.",
+".+++ +++. .+@@+.....+@@+.",
+".... .... .+@@+. .+@@+.",
+" .+. ... .+@@+. .+@@+.",
+" .+. .+++..+@@+. .+@@+.",
+" .+. .+..++.+@@+.....+@@+.",
+" ... .+.+++.+@@+++++++@@+.",
+" ..+.++++.+@@@@@@@@@@@+.",
+" .++.+++++.+@@@@@@@@@@@+.",
+" .++.++++++.+++++++++++++.",
+" .++++++.................",
+" .+.++++. ",
+" .+..++++. ",
+" .+..++.+. ",
+" .+..++. . ",
+" .+++++. ",
+" .+++++. ",
+" .+++++. ",
+" .++++. ",
+" .+++. ",
+" .... ",
+" .+. ",
+" .+++. ",
+".++..+. ",
+".+++.+. ",
+".+++++. ",
+" .+++. ",
+" ... "};
diff --git a/src/ui/pixmaps/cursor-ellipse.xpm b/src/ui/pixmaps/cursor-ellipse.xpm
new file mode 100644
index 0000000..83a820f
--- /dev/null
+++ b/src/ui/pixmaps/cursor-ellipse.xpm
@@ -0,0 +1,40 @@
+/* XPM */
+static char const *cursor_ellipse_xpm[] = {
+"32 32 5 1 4 4",
+" c None",
+". c #FFFFFF",
+"% c Stroke",
+"* c Fill",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ....... ",
+" ... ....%%%%%.... ",
+" ..%%%*****%%%.. ",
+" ..%***********%.. ",
+" ..%*************%.. ",
+" .%***************%. ",
+" .%***************%. ",
+" .%***************%. ",
+" ..%*************%.. ",
+" ..%***********%.. ",
+" ..%%%*****%%%.. ",
+" ....%%%%%.... ",
+" ....... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-eraser.xpm b/src/ui/pixmaps/cursor-eraser.xpm
new file mode 100644
index 0000000..b3f8f2d
--- /dev/null
+++ b/src/ui/pixmaps/cursor-eraser.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_eraser_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ... ",
+" .+. .+++. ",
+" .+. .+...+. ",
+" ... .+.....+. ",
+" .+.......+. ",
+" .+.........+. ",
+" .++..........+. ",
+" .++...........+. ",
+" .+.+...........+. ",
+" .+..+..........++. ",
+" .+...+.........++. ",
+" .+...+.......+.+. ",
+" .+...+.....+..+. ",
+" .+...+...+...+. ",
+" .+...+++...+. ",
+" .+...+...+. ",
+" .+..+..+. ",
+" .+.+.+. ",
+" .+++. ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-gradient-add.xpm b/src/ui/pixmaps/cursor-gradient-add.xpm
new file mode 100644
index 0000000..ea8341b
--- /dev/null
+++ b/src/ui/pixmaps/cursor-gradient-add.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_gradient_add_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ..... ",
+" ... .+++. ",
+" .+.+. ",
+" .+++. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+++. ",
+" .+.+. ",
+" .+++. ",
+" ..... ",
+" ... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ......+...... ",
+" .+++++++++++. ",
+" ......+...... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ... "};
diff --git a/src/ui/pixmaps/cursor-gradient.xpm b/src/ui/pixmaps/cursor-gradient.xpm
new file mode 100644
index 0000000..92c9a9a
--- /dev/null
+++ b/src/ui/pixmaps/cursor-gradient.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_gradient_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ..... ",
+" ... .+++. ",
+" .+.+. ",
+" .+++. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+++. ",
+" .+.+. ",
+" .+++. ",
+" ..... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-measure.xpm b/src/ui/pixmaps/cursor-measure.xpm
new file mode 100644
index 0000000..9f6497c
--- /dev/null
+++ b/src/ui/pixmaps/cursor-measure.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_measure_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. .. ",
+" ... .++. ",
+" .+..+. ",
+" .+....+. ",
+" .+..+...+. ",
+" .+.+.....+. ",
+" .+.......+. ",
+" .+.+.....+. ",
+" .+...+...+. ",
+" .+.+.....+. ",
+" .+.......+. ",
+" .+.+.....+. ",
+" .+...+...+. ",
+" .+.+.....+. ",
+" .+.......+. ",
+" .+.+.....+. ",
+" .+...+.+. ",
+" .+.+.+. ",
+" .+.+. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-node-d.xpm b/src/ui/pixmaps/cursor-node-d.xpm
new file mode 100644
index 0000000..e28bc73
--- /dev/null
+++ b/src/ui/pixmaps/cursor-node-d.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_node_d_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" . ",
+".+. ",
+" .+. ",
+" .++. ",
+" .++. ",
+" .+++. ",
+" .+++. ",
+" .++++. ",
+" .++++. ",
+" .+++++. ",
+" .+++++. ",
+" .++++++. ",
+" .+++++. ",
+" .+++.. ",
+" .+. ++ ",
+" . +++..+++ ",
+" +..+..+..+ ",
+" ++..+..+..++ ",
+" +.+..+..+..+.+ ",
+" +.+..+..+..+.+ ",
+" +............+ ",
+" +............+ ",
+" +...........+ ",
+" +..........+ ",
+" +.........+ ",
+" +........+ ",
+" +.......+ ",
+" +.......+ ",
+" +++++++++ ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-node.xpm b/src/ui/pixmaps/cursor-node.xpm
new file mode 100644
index 0000000..4d3bd94
--- /dev/null
+++ b/src/ui/pixmaps/cursor-node.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_node_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" . ",
+".+. ",
+" .+. ",
+" .++. ",
+" .++. ",
+" .+++. ",
+" .+++. ",
+" .++++. ",
+" .++++. ",
+" .+++++. ",
+" .+++++. ",
+" .++++++. ",
+" .+++++. ",
+" .+++.. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-paintbucket.xpm b/src/ui/pixmaps/cursor-paintbucket.xpm
new file mode 100644
index 0000000..0c6767f
--- /dev/null
+++ b/src/ui/pixmaps/cursor-paintbucket.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_paintbucket_xpm[] = {
+"32 32 3 1 11 30",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" ",
+" ",
+" ++ ++++ ",
+" +..+ ++....+ ",
+" +.++.+ ++..++++.+ ",
+" +.++.++..+++++++.+ ",
+" +.++...+++++++++.+ ",
+" +.++...+++++++++++.+ ",
+" +...++.++++++++++++.+ ",
+" +...+++.++++++++++++.+ ",
+" +.+..+++.+++++++++++.+ ",
+" +.++..++.++++++++++++.+ ",
+" +.+++.++.++++++++++++.+ ",
+" +.+++.....+++++++++++.+ ",
+" +.++++..+.+++++++++++.+ ",
+" +.++++..++++++++++++.+ ",
+" +.++++.++++++++++++..+ ",
+" +..+++..+++++++++..++ ",
+" +.++++.+++++++..++ ",
+" +..+++.+++++..++ ",
+" +.++..+++..++ ",
+" +.....+..++ ",
+" +......++ ",
+" +....++ ",
+" +...+ ",
+" +...+ ",
+" +..+ ",
+" +.+ ",
+" +.+ ",
+" + ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-pen.xpm b/src/ui/pixmaps/cursor-pen.xpm
new file mode 100644
index 0000000..79b68e9
--- /dev/null
+++ b/src/ui/pixmaps/cursor-pen.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_pen_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. .... ",
+" ... .+++... ",
+" .+..+++.. ",
+" .+.+...++.. ",
+" .+.+....++. ",
+" .+..+.....+. ",
+" .+...+....+. ",
+" .+...++...+. ",
+" .+...++...+.. ",
+" .+.......+.+. ",
+" .+........+.+. ",
+" .++.....++..+. ",
+" ..+++.++....+. ",
+" ...++.+.++. ",
+" .++++.++. ",
+" .+++++. ",
+" .+++. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-pencil.xpm b/src/ui/pixmaps/cursor-pencil.xpm
new file mode 100644
index 0000000..92c6331
--- /dev/null
+++ b/src/ui/pixmaps/cursor-pencil.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static const char * cursor_pencil_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ... ",
+" ... .++... ",
+" .+++++... ",
+" .+++.+++.. ",
+" .++.....++. ",
+" .+........+. ",
+" .+......+.+. ",
+" .+....+++..+. ",
+" .+...+...+..+. ",
+" .+..+....+..+. ",
+" .+.++.....+..+. ",
+" .+..+.....+. +. ",
+" .+..+.....+. ",
+" .+..+... .+. ",
+" .+..+. . . ",
+" .+..+. . ",
+" .+ .+. ",
+" .+ . ",
+" . ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-rect.xpm b/src/ui/pixmaps/cursor-rect.xpm
new file mode 100644
index 0000000..e54b7ce
--- /dev/null
+++ b/src/ui/pixmaps/cursor-rect.xpm
@@ -0,0 +1,40 @@
+/* XPM */
+static char const *cursor_rect_xpm[] = {
+"32 32 5 1 4 4",
+" c None",
+". c #FFFFFF",
+"% c Stroke",
+"* c Fill",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ................. ",
+" ... .%%%%%%%%%%%%%%%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%*************%. ",
+" .%%%%%%%%%%%%%%%. ",
+" ................. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-select-d.xpm b/src/ui/pixmaps/cursor-select-d.xpm
new file mode 100644
index 0000000..e84e4de
--- /dev/null
+++ b/src/ui/pixmaps/cursor-select-d.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_select_d_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+". ",
+".. ",
+".+. ",
+".++. ",
+".+++. ",
+".++++. ",
+".+++++. ",
+".++++++. ",
+".+++++++. ",
+".++++++++. ",
+".+++++++++. ",
+".++++++..... ",
+".++++++. ",
+".++..++. ",
+".+. .+++. ",
+".. .++. ",
+". .+++. . ",
+" .+. .+. ",
+" .. .+++. ",
+" .+++++. ",
+" . ..+.. . ",
+" .+. .+. .+. ",
+" .++...+...++. ",
+" .+++++++++++++. ",
+" .++...+...++. ",
+" .+. .+. .+. ",
+" . ..+.. . ",
+" .+++++. ",
+" .+++. ",
+" .+. ",
+" . ",
+" "};
diff --git a/src/ui/pixmaps/cursor-select-m.xpm b/src/ui/pixmaps/cursor-select-m.xpm
new file mode 100644
index 0000000..8dd8fb0
--- /dev/null
+++ b/src/ui/pixmaps/cursor-select-m.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_select_m_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+". ",
+".. ",
+".+. ",
+".++. ",
+".+++. ",
+".++++. ",
+".+++++. ",
+".++++++. ",
+".+++++++. ",
+".++++++++. ",
+".+++++++++. ",
+".++++++..... ",
+".++++++. ",
+".++..++. ",
+".+. .+++. ",
+".. .++. ",
+". .+++. + ",
+" .+. +.+ ",
+" .. +...+ ",
+" +.....+ ",
+" + ++.++ + ",
+" +.+ +.+ +.+ ",
+" +..+++.+++..+ ",
+" +.............+ ",
+" +..+++.+++..+ ",
+" +.+ +.+ +.+ ",
+" + ++.++ + ",
+" +.....+ ",
+" +...+ ",
+" +.+ ",
+" + ",
+" "};
diff --git a/src/ui/pixmaps/cursor-select.xpm b/src/ui/pixmaps/cursor-select.xpm
new file mode 100644
index 0000000..36b40c6
--- /dev/null
+++ b/src/ui/pixmaps/cursor-select.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_select_xpm[] = {
+"32 32 3 1 1 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+". ",
+".. ",
+".+. ",
+".++. ",
+".+++. ",
+".++++. ",
+".+++++. ",
+".++++++. ",
+".+++++++. ",
+".++++++++. ",
+".+++++++++. ",
+".++++++..... ",
+".++++++. ",
+".++..++. ",
+".+. .+++. ",
+".. .++. ",
+". .+++. ",
+" .+. ",
+" .. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-spiral.xpm b/src/ui/pixmaps/cursor-spiral.xpm
new file mode 100644
index 0000000..1eaef1e
--- /dev/null
+++ b/src/ui/pixmaps/cursor-spiral.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_spiral_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ....... ",
+" .+. ...+++++... ",
+" ... ..++.....++.. ",
+" .+.........+.. ",
+" ..+...++++...+. ",
+" .+...+....+..+.. ",
+" .+..+......+..+. ",
+" .+..+......+..+. ",
+" .+..+...+..+..+. ",
+" ..+..+++..+..+.. ",
+" .+......+...+. ",
+" ..++...+...+.. ",
+" ...+++...+.. ",
+" ......+.. ",
+" .... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-spray-move.xpm b/src/ui/pixmaps/cursor-spray-move.xpm
new file mode 100644
index 0000000..0c91839
--- /dev/null
+++ b/src/ui/pixmaps/cursor-spray-move.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_spray_move_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ",
+" ... ... ... ... ",
+" ..+. ..+. ..+. ..+. ",
+" .+++ .+++ .+++ .+++ ",
+" ..+. .+++ .+++ ..+ ",
+" .... .+++ .+++ .... ",
+" ..+. ..+ ..+ ..+. ",
+" .+++ ..+. ..+. .+++ ",
+" ..+ .+++ .+++ ..+ ",
+" .... ..+ ..+ .... ",
+" ..+. .... .... ..+. ",
+" .+++ ..+. ..+. .+++ ",
+" ..+ .+++ .+++ ..+ ",
+" ..+ ..+ ",
+" ... ... ",
+" ..+. ..+. ",
+" .+++ .+++ ",
+" ..+ ..+ ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-spray.xpm b/src/ui/pixmaps/cursor-spray.xpm
new file mode 100644
index 0000000..19a4099
--- /dev/null
+++ b/src/ui/pixmaps/cursor-spray.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_spray_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... .+. +. ",
+" .+.+.+.+. ",
+" .+...+...+. ",
+" +...+.....+. ",
+" . . . .+.+.......+. ",
+" + + + .+.........+. ",
+" . . . .+...........+. ",
+" + + .+.............+. ",
+" . . . .+.............+. ",
+" + + + .+.............+. ",
+" . . .+.............+. ",
+" .+.............+. ",
+" .+.............+. ",
+" .+...........+. ",
+" .+.........+. ",
+" .+.......+. ",
+" .+.....+. ",
+" .+...+. ",
+" .+.+. ",
+" .+. ",
+" . ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-star.xpm b/src/ui/pixmaps/cursor-star.xpm
new file mode 100644
index 0000000..c9f2e85
--- /dev/null
+++ b/src/ui/pixmaps/cursor-star.xpm
@@ -0,0 +1,40 @@
+/* XPM */
+static char const *cursor_star_xpm[] = {
+"32 32 5 1 4 4",
+" c None",
+". c #FFFFFF",
+"% c Stroke",
+"* c Fill",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... .. ",
+" .+. .%%. ",
+" .+. .%%. ",
+" ... .%%. ",
+" .%**%* ",
+" ........%**%........ ",
+" .%%%%%%****%%%%%%. ",
+" .%%**********%%. ",
+" ..%%******%%.. ",
+" .%******%. ",
+" .%******%. ",
+" .%***%%***%. ",
+" .%*%%..%%*%. ",
+" .%*%.. ..%*%. ",
+" .%%. .%%. ",
+" .%. .%. ",
+" .. .. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-text-insert.xpm b/src/ui/pixmaps/cursor-text-insert.xpm
new file mode 100644
index 0000000..488791e
--- /dev/null
+++ b/src/ui/pixmaps/cursor-text-insert.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_text_insert_xpm[] = {
+"32 32 3 1 7 10",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ....... ",
+" .+++.+++. ",
+" ...+... ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ...+... ",
+" .+++.+++. ",
+" ....... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-text.xpm b/src/ui/pixmaps/cursor-text.xpm
new file mode 100644
index 0000000..6d74ae3
--- /dev/null
+++ b/src/ui/pixmaps/cursor-text.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_text_xpm[] = {
+"32 32 3 1 7 7",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" . ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" ..... ..... ",
+".+++++ +++++. ",
+" ..... ..... ",
+" .+. ",
+" .+. ",
+" .+. .... ",
+" .+. .++++. ",
+" .+. .++++. ",
+" . .++++++. ",
+" .++++++. ",
+" .++++++. ",
+" .+++..+++. ",
+" .+++..+++. ",
+" .++++++++. ",
+" .++++++++++. ",
+" .+++....+++. ",
+" ... ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-attract.xpm b/src/ui/pixmaps/cursor-tweak-attract.xpm
new file mode 100644
index 0000000..264360b
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-attract.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_attract_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ",
+" ..... .... ",
+" .++++.. ..++++ ",
+" .....++.....++.... ",
+" .......+++++...... ",
+" .++++.........++++ ",
+" .....++.....++.... ",
+" ..+++++.. ",
+" ....... ",
+" ....... ",
+" ..+++++.. ",
+" .....++.....++.... ",
+" .++++.........++++ ",
+" .......+++++...... ",
+" .....++.....++.... ",
+" .++++.. ..++++ ",
+" ..... .... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-color.xpm b/src/ui/pixmaps/cursor-tweak-color.xpm
new file mode 100644
index 0000000..4bce642
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-color.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_color_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ......... ",
+" ... ........... ",
+" .......+....... ",
+" ...+.+..+.+.... ",
+" .......+.+..+.... ",
+" ....+.++.+.+...+... ",
+" ....+..+.+++++..... ",
+" .....+++++.+..+.... ",
+" ...+..+.++++++.+... ",
+" ....++.+++.+.+..... ",
+" .....++.+++++.++... ",
+" ...+..++.++.+...... ",
+" ....+..++.+.++.+... ",
+" ......+..+.+....... ",
+" ......+.+.+.+.... ",
+" ...+........... ",
+" .....+..+...... ",
+" ........... ",
+" ......... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-less.xpm b/src/ui/pixmaps/cursor-tweak-less.xpm
new file mode 100644
index 0000000..c669bca
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-less.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_less_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ",
+" .. .. ",
+" .+. .+. ",
+" .+. .+. ",
+" .+. .+. ",
+" .+. ...... .+. ",
+" .+..++++..+. ",
+" .+++ +++. ",
+" ..+ . . +.. ",
+" .++.+..+.++. ",
+" .+ .++. +. ",
+" .+ .++. +. ",
+" .++.+..+.++. ",
+" ..+ . . +.. ",
+" .+++ +++. ",
+" .+..++++..+. ",
+" .+. ...... .+. ",
+" .+. .+. ",
+" .+. .+. ",
+" .+. .+. ",
+" .. .. ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-more.xpm b/src/ui/pixmaps/cursor-tweak-more.xpm
new file mode 100644
index 0000000..6321ce9
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-more.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_more_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ...... ",
+" .+. ..++++.. ",
+"....+.... .+++..+++. ",
+".+++ +++. ..+.. ..+.. ",
+"....+.... .++. .++. ",
+" .+. .+. .+. ",
+" .+. .+. .+. ",
+" ... .++. .++. ",
+" ..+.. ..+.. ",
+" .+++..+++. ",
+" ..++++.. ",
+" ... .+ ...... .. ",
+" .+. .+. .+. ",
+" .+..+. +. .+. .+ ",
+" .+.+. .+..+..+. ",
+" .++.... .+.+.+. ",
+" .+++++. .+++. ",
+" ...... .+. ",
+" ...... . ",
+" ..++++.. ...... ",
+" .+++..+++. ..++++.. ",
+" ..+.. ..+.. .+++..+++. ",
+" .++. .++. ..+.. ..+..",
+" .+. .+. .++. .++.",
+" .+. .+. .+. .+.",
+" .++. .++. .+. .+.",
+" ..+.. ..+.. .++. .++.",
+" .+++..+++. ..+.. ..+..",
+" ..++++.. .+++..+++. ",
+" ...... ..++++.. ",
+" ...... "};
diff --git a/src/ui/pixmaps/cursor-tweak-move-in.xpm b/src/ui/pixmaps/cursor-tweak-move-in.xpm
new file mode 100644
index 0000000..d80fa03
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-move-in.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_move_in_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ........ ",
+" .+++++++ ",
+" .+++ ",
+" .++++ ",
+" .+.+++ ",
+" .+..+++ ",
+" .+...+++ ",
+" .+. ..+++ ",
+" .. ..+++ ",
+" ..+++ ",
+" ..+++ ",
+" ..+++ ",
+" ..++ ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-move-jitter.xpm b/src/ui/pixmaps/cursor-tweak-move-jitter.xpm
new file mode 100644
index 0000000..7f0f811
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-move-jitter.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_move_jitter_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. + ",
+" .+. +++ ",
+"....+.... +.+.+ +++++++ ",
+".+++ +++. +..+ .+ ....+++ ",
+"....+.... . .+ . ++.+ ",
+" .+. .+ ++..+ ",
+" .+. .+ ++. .+ ",
+" ... ++. .+ ",
+" ++. .+ ",
+" ++ +. .. ",
+" .++ ",
+" ..++ ",
+" ..++ ",
+" ..++ ",
+" ..++ + ",
+" ..++ + ",
+" .. ..++ + ",
+" .. .++ ..++ + ",
+" +. ..++ ..+++ ",
+" +. .++ ..++ .+ ",
+" +..++ .+++++++. .+ ",
+" +.++ ......... .+ ",
+" +++ .+ ",
+" ++..... + .+ + ",
+" +++++++ + .+.+ +. ",
+" +. .+++. ",
+" +. .+. ",
+" .+++++ . ",
+" .+... ",
+" .+ ",
+" .+ "};
diff --git a/src/ui/pixmaps/cursor-tweak-move-out.xpm b/src/ui/pixmaps/cursor-tweak-move-out.xpm
new file mode 100644
index 0000000..1fa0293
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-move-out.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_move_out_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ",
+" ",
+" ++ ",
+" +++ ",
+" .+++ ",
+" ..+++ ",
+" ..+++ ",
+" ..+++ + ",
+" ..+++ + ",
+" ..+++ + ",
+" ..+++ + ",
+" ..++++ ",
+" ..+++ ",
+" .+++++++. ",
+" ......... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-move.xpm b/src/ui/pixmaps/cursor-tweak-move.xpm
new file mode 100644
index 0000000..67b1098
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-move.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_move_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... +++++++ ",
+".+++ +++. ....+++ ",
+"....+.... ++.+ ",
+" .+. ++..+ ",
+" .+. ++. .+ ",
+" ... ++. .+ ",
+" ++. .+ ",
+" ++ +. .. ",
+" .++ ",
+" ..++ ",
+" ..++ ",
+" ..++ ",
+" ..++ + ",
+" ..++ + ",
+" .. ..++ + ",
+" .. .++ ..++ + ",
+" +. ..++ ..+++ ",
+" +. .++ ..++ ",
+" +..++ .+++++++. ",
+" +.++ ......... ",
+" +++ ",
+" ++..... ",
+" +++++++ ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-push.xpm b/src/ui/pixmaps/cursor-tweak-push.xpm
new file mode 100644
index 0000000..bf331e5
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-push.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_push_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ... ",
+" ... ..... ",
+" ..+++.. ",
+" ..+...+.. ",
+" .....++. .++.... ",
+" .++++.. ..++++ ",
+" ..... ... .... ",
+" ..... ",
+" ..+++.. ",
+" ..+...+.. ",
+" .....++. .++.... ",
+" .++++.. ..++++ ",
+" ..... .... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-repel.xpm b/src/ui/pixmaps/cursor-tweak-repel.xpm
new file mode 100644
index 0000000..5dccefd
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-repel.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_repel_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ....... ",
+" ..+++++.. ",
+" .....++.....++..... ",
+" .++++.........++++. ",
+" .......+++++....... ",
+" .....++.....++..... ",
+" .++++.. ..++++. ",
+" ..... ..... ",
+" ",
+" ..... ..... ",
+" .++++.. ..++++. ",
+" .....++.....++..... ",
+" .......+++++....... ",
+" .++++.........++++. ",
+" .....++.....++..... ",
+" ..+++++.. ",
+" ....... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm b/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm
new file mode 100644
index 0000000..ecbbde8
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_rotate_clockwise_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ... ",
+" .+. .+. ",
+" .+. .+. ",
+" ... .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .. .++. ",
+" .+. .++.. ",
+" .+. .++. ",
+" .+. .++. ",
+" .+......++. ",
+" .+++++++++. ",
+" .+........ ",
+" .+. ",
+" .+. ",
+" .+. ",
+" .. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm b/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm
new file mode 100644
index 0000000..a3c2208
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_rotate_counterclockwise_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. . ",
+"....+.... .+. ",
+" .+. .+++. ",
+" .+. .+.+.+. ",
+" ... .+..+..+. ",
+" .+. .+. .+. ",
+" .. .+. .. ",
+" .+. ",
+" .+. ",
+" .++. ",
+" .++.. ",
+" .++. ",
+" .++. ",
+" .........++. ",
+" .+++++++++. ",
+" ........... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-roughen.xpm b/src/ui/pixmaps/cursor-tweak-roughen.xpm
new file mode 100644
index 0000000..1581b95
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-roughen.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_roughen_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ... ",
+" ....+... ",
+" . .+ +..+... ",
+" ...+.+++.+..+.+... ",
+" .++.+..+ . .+++++ ",
+" ... . ..... ",
+" ",
+" ",
+" .. ",
+" ........++.. ..... ",
+" .++++..+ +...++++ ",
+" .....++.+..+++.... ",
+" ......+.. ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-scale-down.xpm b/src/ui/pixmaps/cursor-tweak-scale-down.xpm
new file mode 100644
index 0000000..b1b15af
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-scale-down.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_scale_down_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. .++++++++++++ ",
+"....+.... .+. + ",
+".+++ +++. .+. + ",
+"....+.... .+. + ",
+" .+. .+. + ",
+" .+. .+. + ",
+" ... .+. + ",
+" .+. + ",
+" .+. + ",
+" .+. + ",
+" .+..........+ ",
+" .++++++++++++ ",
+" ............. ",
+" ",
+" +. ",
+" +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .++. ",
+" .+++++ ",
+" ..... ",
+" .+++++ ",
+" .+. + ",
+" .+. + ",
+" .+...+ ",
+" .+++++ ",
+" ...... ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-scale-up.xpm b/src/ui/pixmaps/cursor-tweak-scale-up.xpm
new file mode 100644
index 0000000..dcc73c1
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-scale-up.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const * cursor_tweak_scale_up_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. .++++++++++++ ",
+"....+.... .+. + ",
+".+++ +++. .+. + ",
+"....+.... .+. + ",
+" .+. .+. + ",
+" .+. .+. + ",
+" ... .+. + ",
+" .+. + ",
+" .+. + ",
+" .+. + ",
+" .+..........+ ",
+" .++++++++++++ ",
+" ............. ",
+" ",
+" +++++ ",
+" ++ ",
+" +.+ ",
+" +..+ ",
+" +. .+ ",
+" +. . ",
+" +. ",
+" ",
+" .+++++ ",
+" .+. + ",
+" .+. + ",
+" .+...+ ",
+" .+++++ ",
+" ...... ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-thicken.xpm b/src/ui/pixmaps/cursor-tweak-thicken.xpm
new file mode 100644
index 0000000..ba7a2df
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-thicken.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_thicken_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ... ",
+" ... ..... ",
+" ..+++.. ",
+" ..+...+.. ",
+" .....++. .++.... ",
+" .++++.. ..++++ ",
+" ..... .... ",
+" ",
+" ",
+" ",
+" ..... .... ",
+" .++++.. ..++++ ",
+" .....++. .++.... ",
+" ..+...+.. ",
+" ..+++.. ",
+" ..... ",
+" ... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-tweak-thin.xpm b/src/ui/pixmaps/cursor-tweak-thin.xpm
new file mode 100644
index 0000000..7d10fe7
--- /dev/null
+++ b/src/ui/pixmaps/cursor-tweak-thin.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_thin_xpm[] = {
+"32 32 3 1 4 4",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" .+. ",
+" .+. ",
+"....+.... ",
+".+++ +++. ",
+"....+.... ",
+" .+. ",
+" .+. ",
+" ... ..... .... ",
+" ...... ..... ",
+" .++++.. ..++++ ",
+" .....++.....++.... ",
+" ..+++++.. ",
+" ..... ",
+" ",
+" ",
+" ..... ",
+" ..+++++.. ",
+" .....++.....++.... ",
+" .++++.. ..++++ ",
+" ...... ..... ",
+" ..... .... ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-zoom-out.xpm b/src/ui/pixmaps/cursor-zoom-out.xpm
new file mode 100644
index 0000000..8f35ad0
--- /dev/null
+++ b/src/ui/pixmaps/cursor-zoom-out.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_zoom_out_xpm[] = {
+"32 32 3 1 6 6",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" ..+++.. ",
+" .++ ++. ",
+" .+ +. ",
+" .+ +. ",
+".+ ..... +. ",
+".+ +++++ +. ",
+".+ ..... +. ",
+" .+ +. ",
+" .+ +. ",
+" .++ ++.+.. ",
+" ..+++..+.++. ",
+" ... .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .++. ",
+" .. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/cursor-zoom.xpm b/src/ui/pixmaps/cursor-zoom.xpm
new file mode 100644
index 0000000..e869239
--- /dev/null
+++ b/src/ui/pixmaps/cursor-zoom.xpm
@@ -0,0 +1,38 @@
+/* XPM */
+static char const *cursor_zoom_xpm[] = {
+"32 32 3 1 6 6",
+" c None",
+". c #FFFFFF",
+"+ c #000000",
+" ... ",
+" ..+++.. ",
+" .++ ++. ",
+" .+ +. ",
+" .+ .+. +. ",
+".+ ..+.. +. ",
+".+ +++++ +. ",
+".+ ..+.. +. ",
+" .+ .+. +. ",
+" .+ +. ",
+" .++ ++.+.. ",
+" ..+++..+.++. ",
+" ... .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .+ +. ",
+" .++. ",
+" .. ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" ",
+" "};
diff --git a/src/ui/pixmaps/handles.xpm b/src/ui/pixmaps/handles.xpm
new file mode 100644
index 0000000..9ef2b4b
--- /dev/null
+++ b/src/ui/pixmaps/handles.xpm
@@ -0,0 +1,159 @@
+/* XPM */
+static char const *handle_scale_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" ",
+" ....... ",
+" .+++.. ",
+" .++++. ",
+" .+++++. ",
+" ..+++++. . ",
+" ...+++++... ",
+" . .+++++.. ",
+" .+++++. ",
+" .++++. ",
+" ..+++. ",
+" ....... ",
+" "};
+
+/* XPM */
+static char const *handle_stretch_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" ",
+" ",
+" . . ",
+" .. .. ",
+" ..+...+.. ",
+" ..+++++++.. ",
+"..+++++++++..",
+" ..+++++++.. ",
+" ..+...+.. ",
+" .. .. ",
+" . . ",
+" ",
+" "};
+
+/* XPM */
+static char const *handle_rotate_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" . ",
+" .. ",
+" .... ",
+" ...++.. ",
+" .++++++..",
+" .+++++++. ",
+" .++++..+. ",
+" .+++. . ",
+"...+++. ",
+" ..++++. ",
+" ..++. ",
+" ... ",
+" . "};
+
+/* XPM */
+static char const *handle_skew_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" . . ",
+" .. .. ",
+" ......... ",
+" ..+++++++.. ",
+"..+++++++++..",
+" ..+++++++.. ",
+" ......... ",
+" .. .. ",
+" . . ",
+" ",
+" ",
+" ",
+" "};
+
+/* XPM */
+static char const *handle_center_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" ",
+" . ",
+" . ",
+" . ",
+" ++.++ ",
+" ++.++ ",
+" ..... ..... ",
+" ++.++ ",
+" ++.++ ",
+" . ",
+" . ",
+" . ",
+" "};
+
+/* XPM */
+static char const *handle_align_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" ",
+" ",
+" ",
+" ........... ",
+" .+++++++. ",
+" .+++++. ",
+" .+++. ",
+" .+. ",
+" . ",
+".............",
+".+++++++++++.",
+".............",
+" "};
+
+/* XPM */
+static char const *handle_align_center_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+".............",
+".+++++++++++.",
+".+.........+.",
+".+. + .+.",
+".+. + .+.",
+".+. + .+.",
+".+.+++++++.+.",
+".+. + .+.",
+".+. + .+.",
+".+. + .+.",
+".+.........+.",
+".+++++++++++.",
+"............."};
+
+/* XPM */
+static char const *handle_align_corner_xpm[] = {
+"13 13 3 1",
+" c None",
+". c #000000",
+"+ c #FFFFFF",
+" . ",
+" .. ...",
+" .+. .+.",
+" .++. .+.",
+" .+++. .+.",
+" .++++. .+.",
+" .+++++. .+.",
+"........ .+.",
+" .+.",
+" .+.",
+" ..........+.",
+" .++++++++++.",
+" ............"};
diff --git a/src/ui/pref-pusher.cpp b/src/ui/pref-pusher.cpp
new file mode 100644
index 0000000..3b84ba6
--- /dev/null
+++ b/src/ui/pref-pusher.cpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "pref-pusher.h"
+
+#include <gtk/gtk.h>
+
+namespace Inkscape {
+namespace UI {
+PrefPusher::PrefPusher( GtkToggleAction *act, Glib::ustring const &path, void (*callback)(gpointer), gpointer cbData ) :
+ Observer(path),
+ act(act),
+ callback(callback),
+ cbData(cbData),
+ freeze(false)
+{
+ g_signal_connect_after( G_OBJECT(act), "toggled", G_CALLBACK(toggleCB), this);
+ freeze = true;
+ gtk_toggle_action_set_active( act, Inkscape::Preferences::get()->getBool(observed_path) );
+ freeze = false;
+
+ Inkscape::Preferences::get()->addObserver(*this);
+}
+
+PrefPusher::~PrefPusher()
+{
+ Inkscape::Preferences::get()->removeObserver(*this);
+}
+
+void PrefPusher::toggleCB( GtkToggleAction * /*act*/, PrefPusher *self )
+{
+ if (self) {
+ self->handleToggled();
+ }
+}
+
+void PrefPusher::handleToggled()
+{
+ if (!freeze) {
+ freeze = true;
+ Inkscape::Preferences::get()->setBool(observed_path, gtk_toggle_action_get_active( act ));
+ if (callback) {
+ (*callback)(cbData);
+ }
+ freeze = false;
+ }
+}
+
+void PrefPusher::notify(Inkscape::Preferences::Entry const &newVal)
+{
+ bool newBool = newVal.getBool();
+ bool oldBool = gtk_toggle_action_get_active(act);
+
+ if (!freeze && (newBool != oldBool)) {
+ gtk_toggle_action_set_active( act, newBool );
+ }
+}
+
+}
+}
+
+/*
+ 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/pref-pusher.h b/src/ui/pref-pusher.h
new file mode 100644
index 0000000..0d62266
--- /dev/null
+++ b/src/ui/pref-pusher.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PREF_PUSHER_H
+#define SEEN_PREF_PUSHER_H
+
+#include "preferences.h"
+
+typedef struct _GtkToggleAction GtkToggleAction;
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * A simple mediator class that keeps UI controls matched to the preference values they set.
+ */
+class PrefPusher : public Inkscape::Preferences::Observer
+{
+public:
+ /**
+ * Constructor for a boolean value that syncs to the supplied path.
+ * Initializes the widget to the current preference stored state and registers callbacks
+ * for widget changes and preference changes.
+ *
+ * @param act the widget to synchronize preference with.
+ * @param path the path to the preference the widget is synchronized with.
+ * @param callback function to invoke when changes are pushed.
+ * @param cbData data to be passed on to the callback function.
+ */
+ PrefPusher( GtkToggleAction *act,
+ Glib::ustring const & path,
+ void (*callback)(gpointer) = nullptr,
+ gpointer cbData = nullptr );
+
+ /**
+ * Destructor that unregisters the preference callback.
+ */
+ ~PrefPusher() override;
+
+ /**
+ * Callback method invoked when the preference setting changes.
+ */
+ void notify(Inkscape::Preferences::Entry const &new_val) override;
+
+
+private:
+ /**
+ * Callback hook invoked when the widget changes.
+ *
+ * @param act the toggle action widget that was changed.
+ * @param self the PrefPusher instance the callback was registered to.
+ */
+ static void toggleCB( GtkToggleAction *act, PrefPusher *self );
+
+ /**
+ * Method to handle the widget change.
+ *
+ * @details Sets the observed path, based on the state of the toggle button
+ * and then runs the callback function
+ */
+ void handleToggled();
+
+ GtkToggleAction *act;
+ void (*callback)(gpointer);
+ gpointer cbData;
+ bool freeze;
+};
+
+}
+}
+#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/previewable.h b/src/ui/previewable.h
new file mode 100644
index 0000000..c25f2db
--- /dev/null
+++ b/src/ui/previewable.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef SEEN_PREVIEWABLE_H
+#define SEEN_PREVIEWABLE_H
+/*
+ * A simple interface for previewing representations.
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <gtkmm/widget.h>
+
+#include "widget/preview.h"
+
+namespace Inkscape {
+namespace UI {
+
+enum PreviewStyle {
+ PREVIEW_STYLE_ICON = 0,
+ PREVIEW_STYLE_PREVIEW,
+ PREVIEW_STYLE_NAME,
+ PREVIEW_STYLE_BLURB,
+ PREVIEW_STYLE_ICON_NAME,
+ PREVIEW_STYLE_ICON_BLURB,
+ PREVIEW_STYLE_PREVIEW_NAME,
+ PREVIEW_STYLE_PREVIEW_BLURB
+};
+
+
+class Previewable
+{
+public:
+// TODO need to add some nice parameters
+ virtual ~Previewable() = default;
+ virtual Gtk::Widget* getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view,
+ UI::Widget::PreviewSize size,
+ guint ratio,
+ guint border) = 0;
+};
+
+
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif // SEEN_PREVIEWABLE_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/previewholder.cpp b/src/ui/previewholder.cpp
new file mode 100644
index 0000000..2d6b8f8
--- /dev/null
+++ b/src/ui/previewholder.cpp
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple interface for previewing representations.
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "previewable.h"
+#include "previewholder.h"
+
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/sizegroup.h>
+#include <gtkmm/scrollbar.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/grid.h>
+
+#define COLUMNS_FOR_SMALL 16
+#define COLUMNS_FOR_LARGE 8
+//#define COLUMNS_FOR_SMALL 48
+//#define COLUMNS_FOR_LARGE 32
+
+
+namespace Inkscape {
+namespace UI {
+
+
+PreviewHolder::PreviewHolder() :
+ Bin(),
+ _scroller(nullptr),
+ _insides(nullptr),
+ _prefCols(0),
+ _updatesFrozen(false),
+ _anchor(SP_ANCHOR_CENTER),
+ _baseSize(UI::Widget::PREVIEW_SIZE_SMALL),
+ _ratio(100),
+ _view(UI::Widget::VIEW_TYPE_LIST),
+ _wrap(false),
+ _border(UI::Widget::BORDER_NONE)
+{
+ set_name( "PreviewHolder" );
+ _scroller = Gtk::manage(new Gtk::ScrolledWindow());
+ _scroller->set_name( "PreviewHolderScroller" );
+ _scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+
+ _insides = Gtk::manage(new Gtk::Grid());
+ _insides->set_name( "PreviewHolderGrid" );
+ _insides->set_column_spacing(8);
+
+ _scroller->set_hexpand();
+ _scroller->set_vexpand();
+ _scroller->add( *_insides );
+
+ // Disable overlay scrolling as the scrollbar covers up swatches.
+ // For some reason this also makes the height 55px.
+ _scroller->set_overlay_scrolling(false);
+
+ add(*_scroller);
+}
+
+PreviewHolder::~PreviewHolder()
+= default;
+
+/**
+ * Translates vertical scrolling into horizontal
+ */
+bool PreviewHolder::on_scroll_event(GdkEventScroll *event)
+{
+ if (_wrap) {
+ return FALSE;
+ }
+
+ // Scroll horizontally by page on mouse wheel
+ auto adj = _scroller->get_hadjustment();
+
+ if (!adj) {
+ return FALSE;
+ }
+
+ double move;
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_LEFT:
+ move = -adj->get_page_size();
+ break;
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_RIGHT:
+ move = adj->get_page_size();
+ break;
+ case GDK_SCROLL_SMOOTH:
+ if (fabs(event->delta_y) <= fabs(event->delta_x)) {
+ return FALSE;
+ }
+#ifdef GDK_WINDOWING_QUARTZ
+ move = event->delta_y;
+#else
+ move = event->delta_y * adj->get_page_size();
+#endif
+ break;
+ default:
+ return FALSE;
+ }
+
+ double value = adj->get_value() + move;
+
+ adj->set_value(value);
+
+ return TRUE;
+}
+
+void PreviewHolder::clear()
+{
+ items.clear();
+ _prefCols = 0;
+ // Kludge to restore scrollbars
+ if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER );
+ }
+ rebuildUI();
+}
+
+/**
+ * Add a Previewable item to the PreviewHolder
+ *
+ * \param[in] preview The Previewable item to add
+ */
+void PreviewHolder::addPreview( Previewable* preview )
+{
+ items.push_back(preview);
+ if ( !_updatesFrozen )
+ {
+ int i = items.size() - 1;
+
+ switch(_view) {
+ case UI::Widget::VIEW_TYPE_LIST:
+ {
+ Gtk::Widget* label = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_BLURB,
+ UI::Widget::VIEW_TYPE_LIST,
+ _baseSize, _ratio, _border));
+ Gtk::Widget* item = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW,
+ UI::Widget::VIEW_TYPE_LIST,
+ _baseSize, _ratio, _border));
+
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, 0, i, 1, 1);
+
+ label->set_hexpand();
+ label->set_valign(Gtk::ALIGN_CENTER);
+ _insides->attach(*label, 1, i, 1, 1);
+ }
+
+ break;
+ case UI::Widget::VIEW_TYPE_GRID:
+ {
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW,
+ UI::Widget::VIEW_TYPE_GRID,
+ _baseSize, _ratio, _border));
+
+ int ncols = 1;
+ int nrows = 1;
+ int col = 0;
+ int row = 0;
+
+ // To get size
+ auto kids = _insides->get_children();
+ int childCount = (int)kids.size();
+ if (childCount > 0 ) {
+
+ // Need already shown widget
+ calcGridSize( kids[0], items.size()+1, ncols, nrows );
+
+ // Column and row for the new widget
+ col = i % ncols;
+ row = i / ncols;
+
+ }
+
+ // Loop through the existing widgets and move them to new location
+ for ( int j = 1; j < childCount; j++ ) {
+ auto target = kids[childCount - (j + 1)];
+ int col2 = j % ncols;
+ int row2 = j / ncols;
+ _insides->remove( *target );
+
+ target->set_hexpand();
+ target->set_vexpand();
+ _insides->attach( *target, col2, row2, 1, 1);
+ }
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, col, row, 1, 1);
+ }
+ }
+
+ _scroller->show_all_children();
+ }
+}
+
+void PreviewHolder::freezeUpdates()
+{
+ _updatesFrozen = true;
+}
+
+void PreviewHolder::thawUpdates()
+{
+ _updatesFrozen = false;
+ rebuildUI();
+}
+
+void
+PreviewHolder::setStyle(UI::Widget::PreviewSize size,
+ UI::Widget::ViewType view,
+ guint ratio,
+ UI::Widget::BorderStyle border )
+{
+ if ( size != _baseSize || view != _view || ratio != _ratio || border != _border ) {
+ _baseSize = size;
+ _view = view;
+ _ratio = ratio;
+ _border = border;
+ // Kludge to restore scrollbars
+ if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER );
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setOrientation(SPAnchorType anchor)
+{
+ if ( _anchor != anchor )
+ {
+ _anchor = anchor;
+ switch ( _anchor )
+ {
+ case SP_ANCHOR_NORTH:
+ case SP_ANCHOR_SOUTH:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER );
+ }
+ break;
+
+ case SP_ANCHOR_EAST:
+ case SP_ANCHOR_WEST:
+ {
+ _scroller->set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC );
+ }
+ break;
+
+ default:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ }
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setWrap( bool wrap )
+{
+ if (_wrap != wrap) {
+ _wrap = wrap;
+ switch ( _anchor )
+ {
+ case SP_ANCHOR_NORTH:
+ case SP_ANCHOR_SOUTH:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER );
+ }
+ break;
+ default:
+ {
+ (void)0;
+ // do nothing;
+ }
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setColumnPref( int cols )
+{
+ _prefCols = cols;
+}
+
+
+/**
+ * Calculate the grid side of a preview holder
+ *
+ * \param[in] item A sample preview widget.
+ * \param[in] itemCount The number of items to pack into the grid.
+ * \param[out] ncols The number of columns in grid.
+ * \param[out] nrows The number of rows in grid.
+ */
+void PreviewHolder::calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows )
+{
+ // Initially set all items in a horizontal row
+ ncols = itemCount;
+ nrows = 1;
+
+ if ( _anchor == SP_ANCHOR_SOUTH || _anchor == SP_ANCHOR_NORTH ) {
+ Gtk::Requisition req;
+ Gtk::Requisition req_natural;
+ _scroller->get_preferred_size(req, req_natural);
+ int currW = _scroller->get_width();
+ if ( currW > req.width ) {
+ req.width = currW;
+ }
+
+ auto hs = _scroller->get_hscrollbar();
+
+ if (_wrap && item != nullptr) {
+
+ // Get width of bar.
+ int width_scroller = _scroller->get_width();
+
+ // Get width of one item (must be visible).
+ int minimum_width_item = 0;
+ int natural_width_item = 0;
+ item->get_preferred_width(minimum_width_item, natural_width_item);
+
+ // Calculate columns and rows.
+ if (natural_width_item < 1) {
+ natural_width_item = 1;
+ }
+ ncols = width_scroller / natural_width_item - 1;
+
+ // On first run, scroller width is not set correct... so we need to fudge it:
+ if (ncols < 2) {
+ ncols = itemCount/2;
+ nrows = 2;
+ } else {
+ nrows = itemCount / ncols;
+ }
+ }
+ } else {
+ ncols = (_baseSize == UI::Widget::PREVIEW_SIZE_SMALL || _baseSize == UI::Widget::PREVIEW_SIZE_TINY) ?
+ COLUMNS_FOR_SMALL : COLUMNS_FOR_LARGE;
+ if ( _prefCols > 0 ) {
+ ncols = _prefCols;
+ }
+ nrows = (itemCount + (ncols - 1)) / ncols;
+ if ( nrows < 1 ) {
+ nrows = 1;
+ }
+ }
+}
+
+void PreviewHolder::rebuildUI()
+{
+ auto children = _insides->get_children();
+ for (auto child : children) {
+ _insides->remove(*child);
+ delete child;
+ }
+
+ _insides->set_column_spacing(0);
+ _insides->set_row_spacing(0);
+ if (_border == UI::Widget::BORDER_WIDE) {
+ _insides->set_column_spacing(1);
+ _insides->set_row_spacing(1);
+ }
+
+ switch (_view) {
+ case UI::Widget::VIEW_TYPE_LIST:
+ {
+ _insides->set_column_spacing(8);
+
+ for ( unsigned int i = 0; i < items.size(); i++ ) {
+ Gtk::Widget* label = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_BLURB, _view, _baseSize, _ratio, _border));
+ //label->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);
+
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, _border));
+
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, 0, i, 1, 1);
+
+ label->set_hexpand();
+ label->set_valign(Gtk::ALIGN_CENTER);
+ _insides->attach(*label, 1, i, 1, 1);
+ }
+ }
+ break;
+
+ case UI::Widget::VIEW_TYPE_GRID:
+ {
+ int col = 0;
+ int row = 0;
+ int ncols = 2;
+ int nrows = 1;
+
+ for ( unsigned int i = 0; i < items.size(); i++ ) {
+
+ // If this is the last row, flag so the previews can draw a bottom
+ UI::Widget::BorderStyle border = ((row == nrows -1) && (_border == UI::Widget::BORDER_SOLID)) ?
+ UI::Widget::BORDER_SOLID_LAST_ROW : _border;
+
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, border));
+ item->set_hexpand();
+ item->set_vexpand();
+
+ if (i == 0) {
+ // We need one item shown before we can call calcGridSize()...
+ _insides->attach( *item, 0, 0, 1, 1);
+ _scroller->show_all_children();
+ calcGridSize( item, items.size(), ncols, nrows );
+ } else {
+ // We've already calculated grid size.
+ _insides->attach( *item, col, row, 1, 1);
+ }
+
+ if ( ++col >= ncols ) {
+ col = 0;
+ row++;
+ }
+ }
+ }
+ }
+
+ _scroller->show_all_children();
+}
+
+
+
+
+
+} //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/previewholder.h b/src/ui/previewholder.h
new file mode 100644
index 0000000..aa1c39a
--- /dev/null
+++ b/src/ui/previewholder.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef SEEN_PREVIEW_HOLDER_H
+#define SEEN_PREVIEW_HOLDER_H
+/*
+ * A simple interface for previewing representations.
+ * Used by Swatches
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/bin.h>
+
+namespace Gtk {
+class Grid;
+class ScrolledWindow;
+}
+
+#include "enums.h"
+
+namespace Inkscape {
+namespace UI {
+
+class Previewable;
+
+class PreviewHolder : public Gtk::Bin
+{
+public:
+ PreviewHolder();
+ ~PreviewHolder() override;
+
+ virtual void clear();
+ virtual void addPreview( Previewable* preview );
+ virtual void freezeUpdates();
+ virtual void thawUpdates();
+ virtual void setStyle(UI::Widget::PreviewSize size,
+ UI::Widget::ViewType view,
+ guint ratio,
+ UI::Widget::BorderStyle border);
+ virtual void setOrientation(SPAnchorType how);
+ virtual int getColumnPref() const { return _prefCols; }
+ virtual void setColumnPref( int cols );
+ virtual UI::Widget::PreviewSize getPreviewSize() const { return _baseSize; }
+ virtual UI::Widget::ViewType getPreviewType() const { return _view; }
+ virtual guint getPreviewRatio() const { return _ratio; }
+ virtual UI::Widget::BorderStyle getPreviewBorder() const { return _border; }
+ virtual void setWrap( bool wrap );
+ virtual bool getWrap() const { return _wrap; }
+
+protected:
+ bool on_scroll_event(GdkEventScroll*) override;
+
+private:
+ void rebuildUI();
+ void calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows );
+
+ std::vector<Previewable*> items;
+ Gtk::ScrolledWindow *_scroller;
+ Gtk::Grid *_insides;
+
+ int _prefCols;
+ bool _updatesFrozen;
+ SPAnchorType _anchor;
+ UI::Widget::PreviewSize _baseSize;
+ guint _ratio;
+ UI::Widget::ViewType _view;
+ bool _wrap;
+ UI::Widget::BorderStyle _border;
+};
+
+} //namespace UI
+} //namespace Inkscape
+
+#endif // SEEN_PREVIEW_HOLDER_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/selected-color.cpp b/src/ui/selected-color.cpp
new file mode 100644
index 0000000..d8bbab1
--- /dev/null
+++ b/src/ui/selected-color.cpp
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selected in color selector widget.
+ * This file was created during the refactoring of SPColorSelector
+ *//*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <cmath>
+
+#include "svg/svg-icc-color.h"
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+
+double const SelectedColor::_EPSILON = 1e-4;
+
+SelectedColor::SelectedColor()
+ : _color(0)
+ , _alpha(1.0)
+ , _held(false)
+ , _virgin(true)
+ , _updating(false)
+{
+
+}
+
+SelectedColor::~SelectedColor() = default;
+
+void SelectedColor::setColor(SPColor const &color)
+{
+ setColorAlpha( color, _alpha);
+}
+
+SPColor SelectedColor::color() const
+{
+ return _color;
+}
+
+void SelectedColor::setAlpha(gfloat alpha)
+{
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+ setColorAlpha( _color, alpha);
+}
+
+gfloat SelectedColor::alpha() const
+{
+ return _alpha;
+}
+
+void SelectedColor::setValue(guint32 value)
+{
+ SPColor color(value);
+ gfloat alpha = SP_RGBA32_A_F(value);
+ setColorAlpha(color, alpha);
+}
+
+guint32 SelectedColor::value() const
+{
+ return color().toRGBA32(_alpha);
+}
+
+void SelectedColor::setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal)
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("SelectedColor::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s)", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():"<null>"), alpha, (emit_signal?"YES":"no"));
+#endif
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+
+ if (_updating) {
+ return;
+ }
+
+#ifdef DUMP_CHANGE_INFO
+ g_message("---- SelectedColor::setColorAlpha virgin:%s !close:%s alpha is:%s",
+ (_virgin?"YES":"no"),
+ (!color.isClose( _color, _EPSILON )?"YES":"no"),
+ ((fabs((_alpha) - (alpha)) >= _EPSILON )?"YES":"no")
+ );
+#endif
+
+ if ( _virgin || !color.isClose( _color, _EPSILON ) ||
+ (fabs((_alpha) - (alpha)) >= _EPSILON )) {
+
+ _virgin = false;
+
+ _color = color;
+ _alpha = alpha;
+
+ if (emit_signal)
+ {
+ _updating = true;
+ if (_held) {
+ signal_dragged.emit();
+ } else {
+ signal_changed.emit();
+ }
+ _updating = false;
+ }
+
+#ifdef DUMP_CHANGE_INFO
+ } else {
+ g_message("++++ SelectedColor::setColorAlpha color:%08x ==> _color:%08X isClose:%s", color.toRGBA32(alpha), _color.toRGBA32(_alpha),
+ (color.isClose( _color, _EPSILON )?"YES":"no"));
+#endif
+ }
+}
+
+void SelectedColor::colorAlpha(SPColor &color, gfloat &alpha) const {
+ color = _color;
+ alpha = _alpha;
+}
+
+void SelectedColor::setHeld(bool held) {
+ if (_updating) {
+ return;
+ }
+ bool grabbed = held && !_held;
+ bool released = !held && _held;
+
+ _held = held;
+
+ _updating = true;
+ if (grabbed) {
+ signal_grabbed.emit();
+ }
+
+ if (released) {
+ signal_released.emit();
+ // signal_changed.emit(); // TODO: signal_changed isn't emitted after dragging!
+ }
+ _updating = false;
+}
+
+void SelectedColor::preserveICC() {
+ _color.icc = _color.icc ? new SVGICCColor(*_color.icc) : nullptr;
+}
+
+}
+}
+
+/*
+ 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/selected-color.h b/src/ui/selected-color.h
new file mode 100644
index 0000000..1a00fc5
--- /dev/null
+++ b/src/ui/selected-color.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selected in color selector widget.
+ * This file was created during the refactoring of SPColorSelector
+ *//*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SELECTED_COLOR
+#define SEEN_SELECTED_COLOR
+
+#include <glib.h>
+#include <sigc++/signal.h>
+
+#include "color.h"
+
+namespace Gtk
+{
+ class Widget;
+}
+
+namespace Inkscape {
+namespace UI {
+
+class SelectedColor {
+public:
+ SelectedColor();
+ virtual ~SelectedColor();
+
+ // By default, disallow copy constructor and assignment operator
+ SelectedColor(SelectedColor const &obj) = delete;
+ SelectedColor& operator=(SelectedColor const &obj) = delete;
+
+ void setColor(SPColor const &color);
+ SPColor color() const;
+
+ void setAlpha(gfloat alpha);
+ gfloat alpha() const;
+
+ void setValue(guint32 value);
+ guint32 value() const;
+
+ void setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal = true);
+ void colorAlpha(SPColor &color, gfloat &alpha) const;
+
+ void setHeld(bool held);
+
+ void preserveICC();
+
+ sigc::signal<void> signal_grabbed;
+ sigc::signal<void> signal_dragged;
+ sigc::signal<void> signal_released;
+ sigc::signal<void> signal_changed;
+
+private:
+ SPColor _color;
+ /**
+ * Color alpha value guaranteed to be in [0, 1].
+ */
+ gfloat _alpha;
+
+ bool _held;
+ /**
+ * This flag is true if no color is set yet
+ */
+ bool _virgin;
+
+ bool _updating;
+
+ static double const _EPSILON;
+};
+
+class ColorSelectorFactory {
+public:
+ virtual ~ColorSelectorFactory() = default;
+
+ virtual Gtk::Widget* createWidget(SelectedColor &color) const = 0;
+ virtual Glib::ustring modeName() const = 0;
+};
+
+}
+}
+
+#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/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp
new file mode 100644
index 0000000..ef395fa
--- /dev/null
+++ b/src/ui/shape-editor-knotholders.cpp
@@ -0,0 +1,1980 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Node editing extension to objects
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Mitsuru Oka
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include "preferences.h"
+#include "desktop.h"
+#include "knotholder.h"
+#include "knot-holder-entity.h"
+#include "style.h"
+
+#include "live_effects/effect.h"
+
+#include "object/box3d.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-offset.h"
+#include "object/sp-pattern.h"
+#include "object/sp-rect.h"
+#include "object/sp-spiral.h"
+#include "object/sp-star.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-tspan.h"
+
+class RectKnotHolder : public KnotHolder {
+public:
+ RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~RectKnotHolder() override = default;;
+};
+
+class Box3DKnotHolder : public KnotHolder {
+public:
+ Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~Box3DKnotHolder() override = default;;
+};
+
+class ArcKnotHolder : public KnotHolder {
+public:
+ ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~ArcKnotHolder() override = default;;
+};
+
+class StarKnotHolder : public KnotHolder {
+public:
+ StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~StarKnotHolder() override = default;;
+};
+
+class SpiralKnotHolder : public KnotHolder {
+public:
+ SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~SpiralKnotHolder() override = default;;
+};
+
+class OffsetKnotHolder : public KnotHolder {
+public:
+ OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~OffsetKnotHolder() override = default;;
+};
+
+class TextKnotHolder : public KnotHolder {
+public:
+ TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~TextKnotHolder() override = default;;
+};
+
+class FlowtextKnotHolder : public KnotHolder {
+public:
+ FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~FlowtextKnotHolder() override = default;;
+};
+
+class MiscKnotHolder : public KnotHolder {
+public:
+ MiscKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~MiscKnotHolder() override = default;;
+};
+
+namespace {
+
+static KnotHolder *sp_lpe_knot_holder(SPLPEItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knot_holder = new KnotHolder(desktop, item, nullptr);
+
+ Inkscape::LivePathEffect::Effect *effect = item->getCurrentLPE();
+ effect->addHandles(knot_holder, item);
+
+ return knot_holder;
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+
+KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knotholder = nullptr;
+
+ if (dynamic_cast<SPRect *>(item)) {
+ knotholder = new RectKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPBox3D *>(item)) {
+ knotholder = new Box3DKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ knotholder = new ArcKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPStar *>(item)) {
+ knotholder = new StarKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ knotholder = new SpiralKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ knotholder = new OffsetKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPText *>(item)) {
+ SPText *text = dynamic_cast<SPText *>(item);
+
+ // Do not allow conversion to 'inline-size' wrapped text if on path!
+ // <textPath> might not be first child if <title> or <desc> is present.
+ bool is_on_path = false;
+ for (auto child : text->childList(false)) {
+ if (dynamic_cast<SPTextPath *>(child)) is_on_path = true;
+ }
+ if (!is_on_path) {
+ knotholder = new TextKnotHolder(desktop, item, nullptr);
+ }
+ } else {
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item);
+ if (flowtext && flowtext->has_internal_frame()) {
+ knotholder = new FlowtextKnotHolder(desktop, flowtext->get_frame(nullptr), nullptr);
+ } else if ((item->style->fill.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getFillPaintServer())) ||
+ (item->style->stroke.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getStrokePaintServer()))) {
+ knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_pattern_knotholder();
+ }
+ }
+ if (!knotholder) knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_filter_knotholder();
+
+ return knotholder;
+}
+
+KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knotholder = nullptr;
+
+ SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item);
+ if (lpe &&
+ lpe->getCurrentLPE() &&
+ lpe->getCurrentLPE()->isVisible() &&
+ lpe->getCurrentLPE()->providesKnotholder()) {
+ knotholder = sp_lpe_knot_holder(lpe, desktop);
+ }
+ return knotholder;
+}
+
+}
+} // namespace Inkscape
+
+/* SPRect */
+
+/* handle for horizontal rounding radius */
+class RectKnotHolderEntityRX : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for vertical rounding radius */
+class RectKnotHolderEntityRY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for width/height adjustment */
+class RectKnotHolderEntityWH : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+};
+
+/* handle for x/y adjustment */
+class RectKnotHolderEntityXY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+/* handle for position */
+class RectKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+RectKnotHolderEntityRX::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed - rect->rx.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(-1, 0)), state);
+
+ if (state & GDK_CONTROL_MASK) {
+ gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0;
+ rect->rx = rect->ry = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, temp);
+ } else {
+ rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0);
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding from rectangle */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("ry", rect->getRepr()->attribute("rx"));
+ }
+
+}
+
+Geom::Point
+RectKnotHolderEntityRY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->ry.computed);
+}
+
+void
+RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(0, 1)), state);
+
+ if (state & GDK_CONTROL_MASK) { // When holding control then rx will be kept equal to ry,
+ // resulting in a perfect circle (and not an ellipse)
+ gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0;
+ rect->rx = rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, temp);
+ } else {
+ if (!rect->rx._set || rect->rx.computed == 0) {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ MIN(rect->height.computed / 2.0, rect->width.computed / 2.0));
+ } else {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ rect->height.computed / 2.0);
+ }
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("rx", rect->getRepr()->attribute("ry"));
+ }
+}
+
+#define SGN(x) ((x)>0?1:((x)<0?-1:0))
+
+static void sp_rect_clamp_radii(SPRect *rect)
+{
+ // clamp rounding radii so that they do not exceed width/height
+ if (2 * rect->rx.computed > rect->width.computed) {
+ rect->rx = 0.5 * rect->width.computed;
+ }
+ if (2 * rect->ry.computed > rect->height.computed) {
+ rect->ry = 0.5 * rect->height.computed;
+ }
+}
+
+Geom::Point
+RectKnotHolderEntityWH::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point s = p;
+
+ if (state & GDK_CONTROL_MASK) {
+ // original width/height when drag started
+ gdouble const w_orig = (origin[Geom::X] - rect->x.computed);
+ gdouble const h_orig = (origin[Geom::Y] - rect->y.computed);
+
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ Geom::Point p_handle(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig + minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->width = MAX(w_orig + minx, 0);
+
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 * ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig + miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->height = MAX(h_orig + miny, 0);
+
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ rect->width = MAX(s[Geom::X] - rect->x.computed, 0);
+ rect->height = MAX(s[Geom::Y] - rect->y.computed, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityWH::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+Geom::Point
+RectKnotHolderEntityXY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ // opposite corner (unmoved)
+ gdouble opposite_x = (rect->x.computed + rect->width.computed);
+ gdouble opposite_y = (rect->y.computed + rect->height.computed);
+
+ // original width/height when drag started
+ gdouble w_orig = opposite_x - origin[Geom::X];
+ gdouble h_orig = opposite_y - origin[Geom::Y];
+
+ Geom::Point s = p;
+ Geom::Point p_handle(rect->x.computed, rect->y.computed);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_CONTROL_MASK) {
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y] + minx / ratio, opposite_y);
+ rect->height = MAX(h_orig - minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->width = MAX(w_orig - minx, 0);
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 *ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X] + miny * ratio, opposite_x);
+ rect->width = MAX(w_orig - miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X], opposite_x);
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->width = MAX(w_orig - minx, 0);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+RectKnotHolderEntityCenter::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + (rect->width.computed / 2.), rect->y.computed + (rect->height.computed / 2.));
+}
+
+void
+RectKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ rect->x = s[Geom::X] - (rect->width.computed / 2.);
+ rect->y = s[Geom::Y] - (rect->height.computed / 2.);
+
+ // No need to call sp_rect_clamp_radii(): width and height haven't changed.
+ // No need to call update_knot(): the knot is set directly by the user.
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+RectKnotHolder::RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ RectKnotHolderEntityRX *entity_rx = new RectKnotHolderEntityRX();
+ RectKnotHolderEntityRY *entity_ry = new RectKnotHolderEntityRY();
+ RectKnotHolderEntityWH *entity_wh = new RectKnotHolderEntityWH();
+ RectKnotHolderEntityXY *entity_xy = new RectKnotHolderEntityXY();
+ RectKnotHolderEntityCenter *entity_center = new RectKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE,
+ _("Adjust the <b>horizontal rounding</b> radius; with <b>Ctrl</b> "
+ "to make the vertical radius the same"),
+ SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR);
+
+ entity_ry->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE,
+ _("Adjust the <b>vertical rounding</b> radius; with <b>Ctrl</b> "
+ "to make the horizontal radius the same"),
+ SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR);
+
+ entity_wh->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER,
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"),
+ SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR);
+
+ entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER,
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"),
+ SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR);
+
+ entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT,
+ _("Drag to move the rectangle"),
+ SP_KNOT_SHAPE_CROSS);
+
+ entity.push_back(entity_rx);
+ entity.push_back(entity_ry);
+ entity.push_back(entity_wh);
+ entity.push_back(entity_xy);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* Box3D (= the new 3D box structure) */
+
+class Box3DKnotHolderEntity : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override = 0;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override = 0;
+
+ Geom::Point knot_get_generic(SPItem *item, unsigned int knot_id) const;
+ void knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &p, unsigned int state);
+};
+
+Geom::Point
+Box3DKnotHolderEntity::knot_get_generic(SPItem *item, unsigned int knot_id) const
+{
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ return box3d_get_corner_screen(box, knot_id);
+ } else {
+ return Geom::Point(); // TODO investigate proper fallback
+ }
+}
+
+void
+Box3DKnotHolderEntity::knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &new_pos, unsigned int state)
+{
+ Geom::Point const s = snap_knot_position(new_pos, state);
+
+ g_assert(item != nullptr);
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ Box3D::Axis movement;
+ if ((knot_id < 4) != (state & GDK_SHIFT_MASK)) {
+ movement = Box3D::XY;
+ } else {
+ movement = Box3D::Z;
+ }
+
+ box3d_set_corner (box, knot_id, s * i2dt, movement, (state & GDK_CONTROL_MASK));
+ box3d_set_z_orders(box);
+ box3d_position_set(box);
+}
+
+class Box3DKnotHolderEntity0 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity1 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity2 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity3 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity4 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity5 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity6 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity7 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+Box3DKnotHolderEntity0::knot_get() const
+{
+ return knot_get_generic(item, 0);
+}
+
+Geom::Point
+Box3DKnotHolderEntity1::knot_get() const
+{
+ return knot_get_generic(item, 1);
+}
+
+Geom::Point
+Box3DKnotHolderEntity2::knot_get() const
+{
+ return knot_get_generic(item, 2);
+}
+
+Geom::Point
+Box3DKnotHolderEntity3::knot_get() const
+{
+ return knot_get_generic(item, 3);
+}
+
+Geom::Point
+Box3DKnotHolderEntity4::knot_get() const
+{
+ return knot_get_generic(item, 4);
+}
+
+Geom::Point
+Box3DKnotHolderEntity5::knot_get() const
+{
+ return knot_get_generic(item, 5);
+}
+
+Geom::Point
+Box3DKnotHolderEntity6::knot_get() const
+{
+ return knot_get_generic(item, 6);
+}
+
+Geom::Point
+Box3DKnotHolderEntity7::knot_get() const
+{
+ return knot_get_generic(item, 7);
+}
+
+Geom::Point
+Box3DKnotHolderEntityCenter::knot_get() const
+{
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ return box3d_get_center_screen(box);
+ } else {
+ return Geom::Point(); // TODO investigate proper fallback
+ }
+}
+
+void
+Box3DKnotHolderEntity0::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 0, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity1::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 1, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity2::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 2, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity3::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 3, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity4::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 4, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity5::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 5, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity6::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 6, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity7::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 7, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntityCenter::knot_set(Geom::Point const &new_pos, Geom::Point const &origin, unsigned int state)
+{
+ Geom::Point const s = snap_knot_position(new_pos, state);
+
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ box3d_set_center(box, s * i2dt, origin * i2dt, !(state & GDK_SHIFT_MASK) ? Box3D::XY : Box3D::Z,
+ state & GDK_CONTROL_MASK);
+
+ box3d_set_z_orders(box);
+ box3d_position_set(box);
+}
+
+Box3DKnotHolder::Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ Box3DKnotHolderEntity0 *entity_corner0 = new Box3DKnotHolderEntity0();
+ Box3DKnotHolderEntity1 *entity_corner1 = new Box3DKnotHolderEntity1();
+ Box3DKnotHolderEntity2 *entity_corner2 = new Box3DKnotHolderEntity2();
+ Box3DKnotHolderEntity3 *entity_corner3 = new Box3DKnotHolderEntity3();
+ Box3DKnotHolderEntity4 *entity_corner4 = new Box3DKnotHolderEntity4();
+ Box3DKnotHolderEntity5 *entity_corner5 = new Box3DKnotHolderEntity5();
+ Box3DKnotHolderEntity6 *entity_corner6 = new Box3DKnotHolderEntity6();
+ Box3DKnotHolderEntity7 *entity_corner7 = new Box3DKnotHolderEntity7();
+ Box3DKnotHolderEntityCenter *entity_center = new Box3DKnotHolderEntityCenter();
+
+ entity_corner0->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner1->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner2->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner3->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner4->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner5->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner6->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner7->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT,
+ _("Move the box in perspective"),
+ SP_KNOT_SHAPE_CROSS);
+
+ entity.push_back(entity_corner0);
+ entity.push_back(entity_corner1);
+ entity.push_back(entity_corner2);
+ entity.push_back(entity_corner3);
+ entity.push_back(entity_corner4);
+ entity.push_back(entity_corner5);
+ entity.push_back(entity_corner6);
+ entity.push_back(entity_corner7);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPArc */
+
+class ArcKnotHolderEntityStart : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityEnd : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityRX : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityRY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+/*
+ * return values:
+ * 1 : inside
+ * 0 : on the curves
+ * -1 : outside
+ */
+static gint
+sp_genericellipse_side(SPGenericEllipse *ellipse, Geom::Point const &p)
+{
+ gdouble dx = (p[Geom::X] - ellipse->cx.computed) / ellipse->rx.computed;
+ gdouble dy = (p[Geom::Y] - ellipse->cy.computed) / ellipse->ry.computed;
+
+ gdouble s = dx * dx + dy * dy;
+ // We add a bit of a buffer, so there's a decent chance the user will
+ // be able to adjust the arc without the closed status flipping between
+ // open and closed during micro mouse movements.
+ if (s < 0.75) return 1;
+ if (s > 1.25) return -1;
+ return 0;
+}
+
+void
+ArcKnotHolderEntityStart::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->start - atan2(delta * sc.inverse());
+ arc->start -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->start = std::round(arc->start/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->end -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityStart::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->start);
+}
+
+void
+ArcKnotHolderEntityStart::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->end - atan2(delta * sc.inverse());
+ arc->end -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->end = std::round(arc->end/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->start -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityEnd::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->end);
+}
+
+
+void
+ArcKnotHolderEntityEnd::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+
+void
+ArcKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->rx = fabs( ge->cx.computed - s[Geom::X] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->ry = ge->rx.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRX::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(ge->rx.computed, 0));
+}
+
+void
+ArcKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->ry = ge->rx.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->ry = fabs( ge->cy.computed - s[Geom::Y] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->rx = ge->ry.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRY::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(0, ge->ry.computed));
+}
+
+void
+ArcKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->rx = ge->ry.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->cx = s[Geom::X];
+ ge->cy = s[Geom::Y];
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityCenter::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return Geom::Point(ge->cx.computed, ge->cy.computed);
+}
+
+
+ArcKnotHolder::ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ ArcKnotHolderEntityRX *entity_rx = new ArcKnotHolderEntityRX();
+ ArcKnotHolderEntityRY *entity_ry = new ArcKnotHolderEntityRY();
+ ArcKnotHolderEntityStart *entity_start = new ArcKnotHolderEntityStart();
+ ArcKnotHolderEntityEnd *entity_end = new ArcKnotHolderEntityEnd();
+ ArcKnotHolderEntityCenter *entity_center = new ArcKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER,
+ _("Adjust ellipse <b>width</b>, with <b>Ctrl</b> to make circle"),
+ SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR);
+
+ entity_ry->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER,
+ _("Adjust ellipse <b>height</b>, with <b>Ctrl</b> to make circle"),
+ SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR);
+
+ entity_start->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE,
+ _("Position the <b>start point</b> of the arc or segment; with <b>Shift</b> to move "
+ "with <b>end point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the "
+ "ellipse for arc, <b>outside</b> for segment"),
+ SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR);
+
+ entity_end->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE,
+ _("Position the <b>end point</b> of the arc or segment; with <b>Shift</b> to move "
+ "with <b>start point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the "
+ "ellipse for arc, <b>outside</b> for segment"),
+ SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR);
+
+ entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT,
+ _("Drag to move the ellipse"),
+ SP_KNOT_SHAPE_CROSS);
+
+ entity.push_back(entity_rx);
+ entity.push_back(entity_ry);
+ entity.push_back(entity_start);
+ entity.push_back(entity_end);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPStar */
+
+class StarKnotHolderEntity1 : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class StarKnotHolderEntity2 : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class StarKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+void
+StarKnotHolderEntity1::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[0];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[0] = L2(d);
+ } else {
+ star->r[0] = L2(d);
+ star->arg[0] = arg1;
+ star->arg[1] += darg1;
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+StarKnotHolderEntity2::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ if (star->flatsided == false) {
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[1];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = fabs(darg1/(star->arg[0] - star->arg[1]));
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[1] = L2(d);
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ }
+ else {
+ star->r[1] = L2(d);
+ star->arg[1] = atan2(d);
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+}
+
+void
+StarKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ star->center = snap_knot_position(p, state);
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+StarKnotHolderEntity1::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT1, 0);
+
+}
+
+Geom::Point
+StarKnotHolderEntity2::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT2, 0);
+}
+
+Geom::Point
+StarKnotHolderEntityCenter::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return star->center;
+}
+
+static void
+sp_star_knot_click(SPItem *item, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = 0;
+ star->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = 0;
+ star->updateRepr();
+ } else if (state & GDK_CONTROL_MASK) {
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ star->updateRepr();
+ }
+}
+
+void
+StarKnotHolderEntity1::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+void
+StarKnotHolderEntity2::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+StarKnotHolder::StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(item != nullptr);
+
+ StarKnotHolderEntity1 *entity1 = new StarKnotHolderEntity1();
+ entity1->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Adjust the <b>tip radius</b> of the star or polygon; "
+ "with <b>Shift</b> to round; with <b>Alt</b> to randomize"));
+
+ entity.push_back(entity1);
+
+ if (star->flatsided == false) {
+ StarKnotHolderEntity2 *entity2 = new StarKnotHolderEntity2();
+ entity2->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Adjust the <b>base radius</b> of the star; with <b>Ctrl</b> to keep star rays "
+ "radial (no skew); with <b>Shift</b> to round; with <b>Alt</b> to randomize"));
+ entity.push_back(entity2);
+ }
+
+ StarKnotHolderEntityCenter *entity_center = new StarKnotHolderEntityCenter();
+ entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT,
+ _("Drag to move the star"),
+ SP_KNOT_SHAPE_CROSS);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPSpiral */
+
+class SpiralKnotHolderEntityInner : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class SpiralKnotHolderEntityOuter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class SpiralKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+
+/*
+ * set attributes via inner (t=t0) knot point:
+ * [default] increase/decrease inner point
+ * [shift] increase/decrease inner and outer arg synchronizely
+ * [control] constrain inner arg to round per PI/4
+ */
+void
+SpiralKnotHolderEntityInner::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ gdouble moved_y = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_MOD1_MASK) {
+ // adjust divergence by vertical drag, relative to rad
+ if (spiral->rad > 0) {
+ double exp_delta = 0.1*moved_y/(spiral->rad); // arbitrary multiplier to slow it down
+ spiral->exp += exp_delta;
+ if (spiral->exp < 1e-3)
+ spiral->exp = 1e-3;
+ }
+ } else {
+ // roll/unroll from inside
+ gdouble arg_t0;
+ spiral->getPolar(spiral->t0, nullptr, &arg_t0);
+
+ gdouble arg_tmp = atan2(dy, dx) - arg_t0;
+ gdouble arg_t0_new = arg_tmp - floor((arg_tmp+M_PI)/(2.0*M_PI))*2.0*M_PI + arg_t0;
+ spiral->t0 = (arg_t0_new - spiral->arg) / (2.0*M_PI*spiral->revo);
+
+ /* round inner arg per PI/snaps, if CTRL is pressed */
+ if ( ( state & GDK_CONTROL_MASK )
+ && ( fabs(spiral->revo) > SP_EPSILON_2 )
+ && ( snaps != 0 ) ) {
+ gdouble arg = 2.0*M_PI*spiral->revo*spiral->t0 + spiral->arg;
+ double snaps_radian = M_PI/snaps;
+ spiral->t0 = (std::round(arg/snaps_radian)*snaps_radian - spiral->arg)/(2.0*M_PI*spiral->revo);
+ }
+
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/*
+ * set attributes via outer (t=1) knot point:
+ * [default] increase/decrease revolution factor
+ * [control] constrain inner arg to round per PI/4
+ */
+void
+SpiralKnotHolderEntityOuter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ if (state & GDK_SHIFT_MASK) { // rotate without roll/unroll
+ spiral->arg = atan2(dy, dx) - 2.0*M_PI*spiral->revo;
+ if (!(state & GDK_MOD1_MASK)) {
+ // if alt not pressed, change also rad; otherwise it is locked
+ spiral->rad = MAX(hypot(dx, dy), 0.001);
+ }
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ spiral->arg = std::round(spiral->arg/snaps_radian) * snaps_radian;
+ }
+ } else { // roll/unroll
+ // arg of the spiral outer end
+ double arg_1;
+ spiral->getPolar(1, nullptr, &arg_1);
+
+ // its fractional part after the whole turns are subtracted
+ static double _2PI = 2.0 * M_PI;
+ double arg_r = arg_1 - std::round(arg_1/_2PI) * _2PI;
+
+ // arg of the mouse point relative to spiral center
+ double mouse_angle = atan2(dy, dx);
+ if (mouse_angle < 0)
+ mouse_angle += _2PI;
+
+ // snap if ctrl
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ mouse_angle = std::round(mouse_angle/snaps_radian) * snaps_radian;
+ }
+
+ // by how much we want to rotate the outer point
+ double diff = mouse_angle - arg_r;
+ if (diff > M_PI)
+ diff -= _2PI;
+ else if (diff < -M_PI)
+ diff += _2PI;
+
+ // calculate the new rad;
+ // the value of t corresponding to the angle arg_1 + diff:
+ double t_temp = ((arg_1 + diff) - spiral->arg)/(_2PI*spiral->revo);
+ // the rad at that t:
+ double rad_new = 0;
+ if (t_temp > spiral->t0)
+ spiral->getPolar(t_temp, &rad_new, nullptr);
+
+ // change the revo (converting diff from radians to the number of turns)
+ spiral->revo += diff/(2*M_PI);
+ if (spiral->revo < 1e-3)
+ spiral->revo = 1e-3;
+
+ // if alt not pressed and the values are sane, change the rad
+ if (!(state & GDK_MOD1_MASK) && rad_new > 1e-3 && rad_new/spiral->rad < 2) {
+ // adjust t0 too so that the inner point stays unmoved
+ double r0;
+ spiral->getPolar(spiral->t0, &r0, nullptr);
+ spiral->rad = rad_new;
+ spiral->t0 = pow(r0 / spiral->rad, 1.0/spiral->exp);
+ }
+ if (!std::isfinite(spiral->t0)) spiral->t0 = 0.0;
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+SpiralKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ spiral->cx = s[Geom::X];
+ spiral->cy = s[Geom::Y];
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+SpiralKnotHolderEntityInner::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(spiral->t0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityOuter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(1.0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityCenter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return Geom::Point(spiral->cx, spiral->cy);
+}
+
+void
+SpiralKnotHolderEntityInner::knot_click(unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ spiral->exp = 1;
+ spiral->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ spiral->t0 = 0;
+ spiral->updateRepr();
+ }
+}
+
+SpiralKnotHolder::SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SpiralKnotHolderEntityCenter *entity_center = new SpiralKnotHolderEntityCenter();
+ SpiralKnotHolderEntityInner *entity_inner = new SpiralKnotHolderEntityInner();
+ SpiralKnotHolderEntityOuter *entity_outer = new SpiralKnotHolderEntityOuter();
+
+ // NOTE: entity_central and entity_inner can overlap.
+ //
+ // In that case it would be a problem if the center control point was ON
+ // TOP because it would steal the mouse focus and the user would loose the
+ // ability to access the inner control point using only the mouse.
+ //
+ // However if the inner control point is ON TOP, taking focus, the
+ // situation is a lot better: the user can still move the inner control
+ // point with the mouse to regain access to the center control point.
+ //
+ // So, create entity_inner AFTER entity_center; this ensures that
+ // entity_inner gets rendered ON TOP.
+ entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT,
+ _("Drag to move the spiral"),
+ SP_KNOT_SHAPE_CROSS);
+
+ entity_inner->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Roll/unroll the spiral from <b>inside</b>; with <b>Ctrl</b> to snap angle; "
+ "with <b>Alt</b> to converge/diverge"));
+
+ entity_outer->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Roll/unroll the spiral from <b>outside</b>; with <b>Ctrl</b> to snap angle; "
+ "with <b>Shift</b> to scale/rotate; with <b>Alt</b> to lock radius"));
+
+ entity.push_back(entity_center);
+ entity.push_back(entity_inner);
+ entity.push_back(entity_outer);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPOffset */
+
+class OffsetKnotHolderEntity : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+void
+OffsetKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPOffset *offset = dynamic_cast<SPOffset *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point const p_snapped = snap_knot_position(p, state);
+
+ offset->rad = sp_offset_distance_to_original(offset, p_snapped);
+ offset->knot = p_snapped;
+ offset->knotSet = true;
+
+ offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+
+Geom::Point
+OffsetKnotHolderEntity::knot_get() const
+{
+ SPOffset const *offset = dynamic_cast<SPOffset const *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point np;
+ sp_offset_top_point(offset,&np);
+ return np;
+}
+
+OffsetKnotHolder::OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ OffsetKnotHolderEntity *entity_offset = new OffsetKnotHolderEntity();
+ entity_offset->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Adjust the <b>offset distance</b>"));
+ entity.push_back(entity_offset);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+
+/* SPText */
+class TextKnotHolderEntityInlineSize : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+Geom::Point
+TextKnotHolderEntityInlineSize::knot_get() const
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ double inline_size = style->inline_size.computed;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point p(text->attributes.firstXY());
+
+ if (text->has_inline_size()) {
+ // SVG 2 'inline-size'
+
+ // Keep handle at end of text line.
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate (inline_size, 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (inline_size/2.0, 0 );
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-inline_size/2.0, 0 );
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-inline_size, 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, inline_size);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, inline_size/2.0);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -inline_size);
+ }
+ }
+ } else {
+ // Normal single line text.
+ Geom::OptRect bbox = text->geometricBounds(); // Check if this is best.
+ if (bbox) {
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate ((*bbox).width(), 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate ((*bbox).width()/2, 0);
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-(*bbox).width()/2, 0);
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-(*bbox).width(), 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, (*bbox).height());
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, (*bbox).height()/2);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -(*bbox).height());
+ }
+ p += Geom::Point((*bbox).width(), 0); // Keep on right side
+ }
+ }
+ }
+
+ return p;
+}
+
+// Conversion from Inkscape SVG 1.1 to SVG 2 'inline-size'.
+void
+TextKnotHolderEntityInlineSize::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point const s = snap_knot_position(p, state);
+ Geom::Point delta = s - text->attributes.firstXY();
+ double size = 0.0;
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+
+ size = delta[Geom::X];
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ // Do nothing
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ size = -size;
+ } else if ( anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ } else {
+ std::cerr << "TextKnotHolderEntityInlinSize: Should not be reached!" << std::endl;
+ }
+
+ } else {
+ // vertical
+
+ size = delta[Geom::Y];
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ // Do nothing
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ size = -size;
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ }
+ }
+
+ // Size should never be negative
+ if (size < 0.0) {
+ size = 0.0;
+ }
+
+ // Set 'inline-size'.
+ text->style->inline_size.setDouble(size);
+ text->style->inline_size.set = true;
+
+ // Ensure we respect new lines.
+ text->style->white_space.read("pre");
+ text->style->white_space.set = true;
+
+ // Convert sodipodi:role="line" to '\n'.
+ text->sodipodi_to_newline();
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+// Conversion from SVG 2 'inline-size' to Inkscape's SVG 1.1.
+void
+TextKnotHolderEntityInlineSize::knot_click(unsigned int state)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+
+ text->style->inline_size.clear();
+ text->remove_svg11_fallback(); // Else 'x' and 'y' will be interpreted as absolute positions.
+ text->newline_to_sodipodi(); // Convert '\n' to tspans with sodipodi:role="line".
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+ }
+}
+
+class TextKnotHolderEntityShapeInside : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+TextKnotHolderEntityShapeInside::knot_get() const
+{
+ // SVG 2 'shape-inside'. We only get here if there is a rectangle shape.
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+ // we have a crash on undo cration so remove assert
+ // g_assert(text->style->shape_inside.set);
+ Geom::Point p;
+ if (text->style->shape_inside.set) {
+ Geom::OptRect frame = text->get_frame();
+ if (frame) {
+ p = (*frame).corner(2);
+ } else {
+ std::cerr << "TextKnotHolderEntityShapeInside::knot_get(): no frame!" << std::endl;
+ }
+ }
+ return p;
+}
+
+void
+TextKnotHolderEntityShapeInside::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ // Text in a shape: rectangle
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+ g_assert(text->style->shape_inside.set);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Inkscape::XML::Node* rectangle = text->get_first_rectangle();
+ double x = 0.0;
+ double y = 0.0;
+ sp_repr_get_double (rectangle, "x", &x);
+ sp_repr_get_double (rectangle, "y", &y);
+ double width = s[Geom::X] - x;
+ double height = s[Geom::Y] - y;
+ sp_repr_set_svg_double (rectangle, "width", width);
+ sp_repr_set_svg_double (rectangle, "height", height);
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+TextKnotHolder::TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ if (text->style->shape_inside.set) {
+ // 'shape-inside'
+ TextKnotHolderEntityShapeInside *entity_shapeinside = new TextKnotHolderEntityShapeInside();
+
+ entity_shapeinside->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Adjust the <b>rectangular</b> region of the text."),
+ SP_KNOT_SHAPE_DIAMOND, SP_KNOT_MODE_XOR);
+
+ entity.push_back(entity_shapeinside);
+
+ } else {
+ // 'inline-size' or normal text
+ TextKnotHolderEntityInlineSize *entity_inlinesize = new TextKnotHolderEntityInlineSize();
+
+ entity_inlinesize->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Adjust the <b>inline size</b> (line length) of the text."),
+ SP_KNOT_SHAPE_DIAMOND, SP_KNOT_MODE_XOR);
+
+ entity.push_back(entity_inlinesize);
+ }
+}
+
+
+// TODO: this is derived from RectKnotHolderEntityWH because it used the same static function
+// set_internal as the latter before KnotHolderEntity was C++ified. Check whether this also makes
+// sense logically.
+class FlowtextKnotHolderEntity : public RectKnotHolderEntityWH {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+FlowtextKnotHolderEntity::knot_get() const
+{
+ SPRect const *rect = dynamic_cast<SPRect const *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+FlowtextKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+}
+
+FlowtextKnotHolder::FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ g_assert(item != nullptr);
+
+ FlowtextKnotHolderEntity *entity_flowtext = new FlowtextKnotHolderEntity();
+ entity_flowtext->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER,
+ _("Drag to resize the <b>flowed text frame</b>"));
+ entity.push_back(entity_flowtext);
+}
+
+/*
+ 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/shape-editor.cpp b/src/ui/shape-editor.cpp
new file mode 100644
index 0000000..9125bbc
--- /dev/null
+++ b/src/ui/shape-editor.cpp
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Inkscape::ShapeEditor
+ * This is a container class which contains a knotholder for shapes.
+ * It is attached to a single item.
+ *//*
+ * Authors: see git history
+ * bulia byak <buliabyak@users.sf.net>
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "desktop.h"
+#include "document.h"
+#include "knotholder.h"
+#include "live_effects/effect.h"
+#include "object/sp-lpe-item.h"
+
+#include "ui/shape-editor.h"
+#include "xml/node-event-vector.h"
+
+
+namespace Inkscape {
+namespace UI {
+
+KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop);
+KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop);
+
+bool ShapeEditor::_blockSetItem = false;
+
+ShapeEditor::ShapeEditor(SPDesktop *dt, Geom::Affine edit_transform) :
+ desktop(dt),
+ knotholder(nullptr),
+ lpeknotholder(nullptr),
+ knotholder_listener_attached_for(nullptr),
+ lpeknotholder_listener_attached_for(nullptr),
+ _edit_transform(edit_transform)
+{
+}
+
+ShapeEditor::~ShapeEditor() {
+ unset_item();
+}
+
+void ShapeEditor::unset_item(bool keep_knotholder) {
+ if (this->knotholder) {
+ Inkscape::XML::Node *old_repr = this->knotholder->repr;
+ if (old_repr && old_repr == knotholder_listener_attached_for) {
+ sp_repr_remove_listener_by_data(old_repr, this);
+ Inkscape::GC::release(old_repr);
+ knotholder_listener_attached_for = nullptr;
+ }
+
+ if (!keep_knotholder) {
+ delete this->knotholder;
+ this->knotholder = nullptr;
+ }
+ }
+ if (this->lpeknotholder) {
+ Inkscape::XML::Node *old_repr = this->lpeknotholder->repr;
+ if (old_repr && old_repr == lpeknotholder_listener_attached_for) {
+ sp_repr_remove_listener_by_data(old_repr, this);
+ Inkscape::GC::release(old_repr);
+ lpeknotholder_listener_attached_for = nullptr;
+ }
+
+ if (!keep_knotholder) {
+ delete this->lpeknotholder;
+ this->lpeknotholder = nullptr;
+ }
+ }
+}
+
+bool ShapeEditor::has_knotholder() {
+ return this->knotholder != nullptr || this->lpeknotholder != nullptr;
+}
+
+void ShapeEditor::update_knotholder() {
+ if (this->knotholder)
+ this->knotholder->update_knots();
+ if (this->lpeknotholder)
+ this->lpeknotholder->update_knots();
+}
+
+bool ShapeEditor::has_local_change() {
+ return (this->knotholder && this->knotholder->local_change != 0) || (this->lpeknotholder && this->lpeknotholder->local_change != 0);
+}
+
+void ShapeEditor::decrement_local_change() {
+ if (this->knotholder) {
+ this->knotholder->local_change = FALSE;
+ }
+ if (this->lpeknotholder) {
+ this->lpeknotholder->local_change = FALSE;
+ }
+}
+
+void ShapeEditor::event_attr_changed(Inkscape::XML::Node * node, gchar const *name, gchar const *, gchar const *, bool, void *data)
+{
+ g_assert(data);
+ ShapeEditor *sh = static_cast<ShapeEditor *>(data);
+ bool changed_kh = false;
+
+ if (sh->has_knotholder())
+ {
+ changed_kh = !sh->has_local_change();
+ sh->decrement_local_change();
+ if (changed_kh) {
+ sh->reset_item();
+ }
+ }
+}
+
+static Inkscape::XML::NodeEventVector shapeeditor_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ ShapeEditor::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+
+void ShapeEditor::set_item(SPItem *item) {
+ if (_blockSetItem) {
+ return;
+ }
+ // this happens (and should only happen) when for an LPEItem having both knotholder and
+ // nodepath the knotholder is adapted; in this case we don't want to delete the knotholder
+ // since this freezes the handles
+ unset_item(true);
+
+ if (item) {
+ Inkscape::XML::Node *repr;
+ if (!this->knotholder) {
+ // only recreate knotholder if none is present
+ this->knotholder = createKnotHolder(item, desktop);
+ }
+ SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item);
+ if (!(lpe &&
+ lpe->getCurrentLPE() &&
+ lpe->getCurrentLPE()->isVisible() &&
+ lpe->getCurrentLPE()->providesKnotholder()))
+ {
+ delete this->lpeknotholder;
+ this->lpeknotholder = nullptr;
+ }
+ if (!this->lpeknotholder) {
+ // only recreate knotholder if none is present
+ this->lpeknotholder = createLPEKnotHolder(item, desktop);
+ }
+ if (this->knotholder) {
+ this->knotholder->setEditTransform(_edit_transform);
+ this->knotholder->update_knots();
+ // setting new listener
+ repr = this->knotholder->repr;
+ if (repr != knotholder_listener_attached_for) {
+ Inkscape::GC::anchor(repr);
+ sp_repr_add_listener(repr, &shapeeditor_repr_events, this);
+ knotholder_listener_attached_for = repr;
+ }
+ }
+ if (this->lpeknotholder) {
+ this->lpeknotholder->setEditTransform(_edit_transform);
+ this->lpeknotholder->update_knots();
+ // setting new listener
+ repr = this->lpeknotholder->repr;
+ if (repr != lpeknotholder_listener_attached_for) {
+ Inkscape::GC::anchor(repr);
+ sp_repr_add_listener(repr, &shapeeditor_repr_events, this);
+ lpeknotholder_listener_attached_for = repr;
+ }
+ }
+ }
+}
+
+
+/** FIXME: This thing is only called when the item needs to be updated in response to repr change.
+ Why not make a reload function in KnotHolder? */
+void ShapeEditor::reset_item()
+{
+ if (knotholder) {
+ SPObject *obj = desktop->getDocument()->getObjectByRepr(knotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject.
+ set_item(SP_ITEM(obj));
+ } else if (lpeknotholder) {
+ SPObject *obj = desktop->getDocument()->getObjectByRepr(lpeknotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject.
+ set_item(SP_ITEM(obj));
+ }
+}
+
+/**
+ * Returns true if this ShapeEditor has a knot above which the mouse currently hovers.
+ */
+bool ShapeEditor::knot_mouseover() const {
+ if (this->knotholder) {
+ return knotholder->knot_mouseover();
+ }
+ if (this->lpeknotholder) {
+ return lpeknotholder->knot_mouseover();
+ }
+
+ return false;
+}
+
+} // 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 :
diff --git a/src/ui/shape-editor.h b/src/ui/shape-editor.h
new file mode 100644
index 0000000..d867ad9
--- /dev/null
+++ b/src/ui/shape-editor.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Inkscape::ShapeEditor
+ * This is a container class which contains a knotholder for shapes.
+ * It is attached to a single item.
+ *//*
+ * Authors: see git history
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SHAPE_EDITOR_H
+#define SEEN_SHAPE_EDITOR_H
+
+class KnotHolder;
+class LivePathEffectObject;
+class SPDesktop;
+class SPItem;
+
+namespace Inkscape { namespace XML { class Node; }
+namespace UI {
+
+class ShapeEditor {
+public:
+
+ ShapeEditor(SPDesktop *desktop, Geom::Affine edit_transform = Geom::identity());
+ ~ShapeEditor();
+
+ void set_item(SPItem *item);
+ void unset_item(bool keep_knotholder = false);
+
+ void update_knotholder(); //((deprecated))
+
+ bool has_local_change();
+ void decrement_local_change();
+
+ bool knot_mouseover() const;
+ KnotHolder *knotholder;
+ KnotHolder *lpeknotholder;
+ bool has_knotholder();
+ static void blockSetItem(bool b) { _blockSetItem = b; } // kludge
+ static void event_attr_changed(Inkscape::XML::Node * /*repr*/, char const *name, char const * /*old_value*/,
+ char const * /*new_value*/, bool /*is_interactive*/, void *data);
+private:
+ void reset_item();
+ static bool _blockSetItem;
+
+ SPDesktop *desktop;
+ Inkscape::XML::Node *knotholder_listener_attached_for;
+ Inkscape::XML::Node *lpeknotholder_listener_attached_for;
+ Geom::Affine _edit_transform;
+};
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SHAPE_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 :
+
diff --git a/src/ui/simple-pref-pusher.cpp b/src/ui/simple-pref-pusher.cpp
new file mode 100644
index 0000000..bc71f27
--- /dev/null
+++ b/src/ui/simple-pref-pusher.cpp
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "simple-pref-pusher.h"
+
+#include <gtkmm/toggletoolbutton.h>
+
+namespace Inkscape {
+namespace UI {
+SimplePrefPusher::SimplePrefPusher( Gtk::ToggleToolButton *btn, Glib::ustring const &path ) :
+ Observer(path),
+ _btn(btn),
+ freeze(false)
+{
+ freeze = true;
+ _btn->set_active( Inkscape::Preferences::get()->getBool(observed_path) );
+ freeze = false;
+
+ Inkscape::Preferences::get()->addObserver(*this);
+}
+
+SimplePrefPusher::~SimplePrefPusher()
+{
+ Inkscape::Preferences::get()->removeObserver(*this);
+}
+
+void
+SimplePrefPusher::notify(Inkscape::Preferences::Entry const &newVal)
+{
+ bool newBool = newVal.getBool();
+ bool oldBool = _btn->get_active();
+
+ if (!freeze && (newBool != oldBool)) {
+ _btn->set_active(newBool);
+ }
+}
+
+}
+}
+
+/*
+ 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/simple-pref-pusher.h b/src/ui/simple-pref-pusher.h
new file mode 100644
index 0000000..c17c625
--- /dev/null
+++ b/src/ui/simple-pref-pusher.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SIMPLE_PREF_PUSHER_H
+#define SEEN_SIMPLE_PREF_PUSHER_H
+
+#include "preferences.h"
+
+namespace Gtk {
+class ToggleToolButton;
+}
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * A simple mediator class that sets the state of a Gtk::ToggleToolButton when
+ * a preference is changed. Unlike the PrefPusher class, this does not provide
+ * the reverse process, so you still need to write your own handler for the
+ * "toggled" signal on the ToggleToolButton.
+ */
+class SimplePrefPusher : public Inkscape::Preferences::Observer
+{
+public:
+ /**
+ * Constructor for a boolean value that syncs to the supplied path.
+ * Initializes the widget to the current preference stored state and registers callbacks
+ * for widget changes and preference changes.
+ *
+ * @param act the widget to synchronize preference with.
+ * @param path the path to the preference the widget is synchronized with.
+ * @param callback function to invoke when changes are pushed.
+ * @param cbData data to be passed on to the callback function.
+ */
+ SimplePrefPusher(Gtk::ToggleToolButton *btn,
+ Glib::ustring const & path);
+
+ /**
+ * Destructor that unregisters the preference callback.
+ */
+ ~SimplePrefPusher() override;
+
+ /**
+ * Callback method invoked when the preference setting changes.
+ */
+ void notify(Inkscape::Preferences::Entry const &new_val) override;
+
+
+private:
+ Gtk::ToggleToolButton *_btn;
+ bool freeze;
+};
+
+}
+}
+#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/tool-factory.cpp b/src/ui/tool-factory.cpp
new file mode 100644
index 0000000..8c80c39
--- /dev/null
+++ b/src/ui/tool-factory.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Factory for ToolBase tree
+ *
+ * Authors:
+ * Markus Engel
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tool-factory.h"
+
+#include "ui/tools/arc-tool.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/tools/calligraphic-tool.h"
+#include "ui/tools/connector-tool.h"
+#include "ui/tools/dropper-tool.h"
+#include "ui/tools/eraser-tool.h"
+#include "ui/tools/flood-tool.h"
+#include "ui/tools/gradient-tool.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/tools/measure-tool.h"
+#include "ui/tools/mesh-tool.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/pencil-tool.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/spiral-tool.h"
+#include "ui/tools/spray-tool.h"
+#include "ui/tools/star-tool.h"
+#include "ui/tools/text-tool.h"
+#include "ui/tools/tweak-tool.h"
+#include "ui/tools/zoom-tool.h"
+
+using namespace Inkscape::UI::Tools;
+
+ToolBase *ToolFactory::createObject(std::string const& id)
+{
+ ToolBase *tool = nullptr;
+
+ if (id == "/tools/shapes/arc")
+ tool = new ArcTool;
+ else if (id == "/tools/shapes/3dbox")
+ tool = new Box3dTool;
+ else if (id == "/tools/calligraphic")
+ tool = new CalligraphicTool;
+ else if (id == "/tools/connector")
+ tool = new ConnectorTool;
+ else if (id == "/tools/dropper")
+ tool = new DropperTool;
+ else if (id == "/tools/eraser")
+ tool = new EraserTool;
+ else if (id == "/tools/paintbucket")
+ tool = new FloodTool;
+ else if (id == "/tools/gradient")
+ tool = new GradientTool;
+ else if (id == "/tools/lpetool")
+ tool = new LpeTool;
+ else if (id == "/tools/measure")
+ tool = new MeasureTool;
+ else if (id == "/tools/mesh")
+ tool = new MeshTool;
+ else if (id == "/tools/nodes")
+ tool = new NodeTool;
+ else if (id == "/tools/freehand/pencil")
+ tool = new PencilTool;
+ else if (id == "/tools/freehand/pen")
+ tool = new PenTool;
+ else if (id == "/tools/shapes/rect")
+ tool = new RectTool;
+ else if (id == "/tools/select")
+ tool = new SelectTool;
+ else if (id == "/tools/shapes/spiral")
+ tool = new SpiralTool;
+ else if (id == "/tools/spray")
+ tool = new SprayTool;
+ else if (id == "/tools/shapes/star")
+ tool = new StarTool;
+ else if (id == "/tools/text")
+ tool = new TextTool;
+ else if (id == "/tools/tweak")
+ tool = new TweakTool;
+ else if (id == "/tools/zoom")
+ tool = new ZoomTool;
+ else
+ fprintf(stderr, "WARNING: unknown tool: %s", id.c_str());
+
+ return tool;
+}
+
+/*
+ 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/tool-factory.h b/src/ui/tool-factory.h
new file mode 100644
index 0000000..e6dcbd2
--- /dev/null
+++ b/src/ui/tool-factory.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Factory for ToolBase tree
+ *
+ * Authors:
+ * Markus Engel
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef TOOL_FACTORY_SEEN
+#define TOOL_FACTORY_SEEN
+
+#include <string>
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ToolBase;
+
+}
+}
+}
+
+struct ToolFactory {
+ static Inkscape::UI::Tools::ToolBase *createObject(std::string const& id);
+};
+
+
+#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/tool/commit-events.h b/src/ui/tool/commit-events.h
new file mode 100644
index 0000000..37fb861
--- /dev/null
+++ b/src/ui/tool/commit-events.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Commit events.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_COMMIT_EVENTS_H
+#define SEEN_UI_TOOL_COMMIT_EVENTS_H
+
+namespace Inkscape {
+namespace UI {
+
+/// This is used to provide sensible messages on the undo stack.
+enum CommitEvent {
+ COMMIT_MOUSE_MOVE,
+ COMMIT_KEYBOARD_MOVE_X,
+ COMMIT_KEYBOARD_MOVE_Y,
+ COMMIT_MOUSE_SCALE,
+ COMMIT_MOUSE_SCALE_UNIFORM,
+ COMMIT_KEYBOARD_SCALE_UNIFORM,
+ COMMIT_KEYBOARD_SCALE_X,
+ COMMIT_KEYBOARD_SCALE_Y,
+ COMMIT_MOUSE_ROTATE,
+ COMMIT_KEYBOARD_ROTATE,
+ COMMIT_MOUSE_SKEW_X,
+ COMMIT_MOUSE_SKEW_Y,
+ COMMIT_KEYBOARD_SKEW_X,
+ COMMIT_KEYBOARD_SKEW_Y,
+ COMMIT_FLIP_X,
+ COMMIT_FLIP_Y
+};
+
+} // 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/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp
new file mode 100644
index 0000000..186d782
--- /dev/null
+++ b/src/ui/tool/control-point-selection.cpp
@@ -0,0 +1,773 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Node selection - implementation.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <boost/none.hpp>
+#include "ui/tool/selectable-control-point.h"
+#include <2geom/transforms.h>
+#include "desktop.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/tool/node.h"
+
+
+
+#include <gdk/gdkkeysyms.h>
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * @class ControlPointSelection
+ * Group of selected control points.
+ *
+ * Some operations can be performed on all selected points regardless of their type, therefore
+ * this class is also a Manipulator. It handles the transformations of points using
+ * the keyboard.
+ *
+ * The exposed interface is similar to that of an STL set. Internally, a hash map is used.
+ * @todo Correct iterators (that don't expose the connection list)
+ */
+
+/** @var ControlPointSelection::signal_update
+ * Fires when the display needs to be updated to reflect changes.
+ */
+/** @var ControlPointSelection::signal_point_changed
+ * Fires when a control point is added to or removed from the selection.
+ * The first param contains a pointer to the control point that changed sel. state.
+ * The second says whether the point is currently selected.
+ */
+/** @var ControlPointSelection::signal_commit
+ * Fires when a change that needs to be committed to XML happens.
+ */
+
+ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group)
+ : Manipulator(d)
+ , _handles(new TransformHandleSet(d, th_group))
+ , _dragging(false)
+ , _handles_visible(true)
+ , _one_node_handles(false)
+{
+ signal_update.connect( sigc::bind(
+ sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
+ true));
+ ControlPoint::signal_mouseover_change.connect(
+ sigc::hide(
+ sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
+ _handles->signal_transform.connect(
+ sigc::mem_fun(*this, &ControlPointSelection::transform));
+ _handles->signal_commit.connect(
+ sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
+}
+
+ControlPointSelection::~ControlPointSelection()
+{
+ clear();
+ delete _handles;
+}
+
+/** Add a control point to the selection. */
+std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x, bool notify, bool to_update)
+{
+ iterator found = _points.find(x);
+ if (found != _points.end()) {
+ return std::pair<iterator, bool>(found, false);
+ }
+
+ found = _points.insert(x).first;
+ _points_list.push_back(x);
+
+ x->updateState();
+
+ if (to_update) {
+ _update();
+ }
+ if (notify) {
+ signal_selection_changed.emit(std::vector<key_type>(1, x), true);
+ }
+
+ return std::pair<iterator, bool>(found, true);
+}
+
+/** Remove a point from the selection. */
+void ControlPointSelection::erase(iterator pos, bool to_update)
+{
+ SelectableControlPoint *erased = *pos;
+ _points_list.remove(*pos);
+ _points.erase(pos);
+ erased->updateState();
+ if (to_update) {
+ _update();
+ }
+}
+ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k, bool notify)
+{
+ iterator pos = _points.find(k);
+ if (pos == _points.end()) return 0;
+ erase(pos);
+
+ if (notify) {
+ signal_selection_changed.emit(std::vector<key_type>(1, k), false);
+ }
+ return 1;
+}
+void ControlPointSelection::erase(iterator first, iterator last)
+{
+ std::vector<SelectableControlPoint *> out(first, last);
+ while (first != last) {
+ erase(first++, false);
+ }
+ _update();
+ signal_selection_changed.emit(out, false);
+}
+
+/** Remove all points from the selection, making it empty. */
+void ControlPointSelection::clear()
+{
+ if (empty()) {
+ return;
+ }
+
+ std::vector<SelectableControlPoint *> out(begin(), end()); // begin() takes from _points
+ _points.clear();
+ _points_list.clear();
+ for (auto erased : out) {
+ erased->updateState();
+ }
+
+ _update();
+ signal_selection_changed.emit(out, false);
+}
+
+/** Select all points that this selection can contain. */
+void ControlPointSelection::selectAll()
+{
+ for (auto _all_point : _all_points) {
+ insert(_all_point, false, false);
+ }
+ std::vector<SelectableControlPoint *> out(_all_points.begin(), _all_points.end());
+ if (!out.empty()) {
+ _update();
+ signal_selection_changed.emit(out, true);
+ }
+}
+/** Select all points inside the given rectangle (in desktop coordinates). */
+void ControlPointSelection::selectArea(Geom::Rect const &r)
+{
+ std::vector<SelectableControlPoint *> out;
+ for (auto _all_point : _all_points) {
+ if (r.contains(*_all_point)) {
+ insert(_all_point, false, false);
+ out.push_back(_all_point);
+ }
+ }
+ if (!out.empty()) {
+ _update();
+ signal_selection_changed.emit(out, true);
+ }
+}
+/** Unselect all selected points and select all unselected points. */
+void ControlPointSelection::invertSelection()
+{
+ std::vector<SelectableControlPoint *> in, out;
+ for (auto _all_point : _all_points) {
+ if (_all_point->selected()) {
+ in.push_back(_all_point);
+ erase(_all_point);
+ }
+ else {
+ out.push_back(_all_point);
+ insert(_all_point, false, false);
+ }
+ }
+ _update();
+ if (!in.empty())
+ signal_selection_changed.emit(in, false);
+ if (!out.empty())
+ signal_selection_changed.emit(out, true);
+}
+void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
+{
+ bool grow = (dir > 0);
+ Geom::Point p = origin->position();
+ double best_dist = grow ? HUGE_VAL : 0;
+ SelectableControlPoint *match = nullptr;
+ for (auto _all_point : _all_points) {
+ bool selected = _all_point->selected();
+ if (grow && !selected) {
+ double dist = Geom::distance(_all_point->position(), p);
+ if (dist < best_dist) {
+ best_dist = dist;
+ match = _all_point;
+ }
+ }
+ if (!grow && selected) {
+ double dist = Geom::distance(_all_point->position(), p);
+ // use >= to also deselect the origin node when it's the last one selected
+ if (dist >= best_dist) {
+ best_dist = dist;
+ match = _all_point;
+ }
+ }
+ }
+ if (match) {
+ if (grow) insert(match);
+ else erase(match);
+ signal_selection_changed.emit(std::vector<value_type>(1, match), grow);
+ }
+}
+
+/** Transform all selected control points by the given affine transformation. */
+void ControlPointSelection::transform(Geom::Affine const &m)
+{
+ for (auto cur : _points) {
+ cur->transform(m);
+ }
+ _updateBounds();
+ // TODO preserving the rotation radius needs some rethinking...
+ if (_rot_radius) (*_rot_radius) *= m.descrim();
+ if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
+ signal_update.emit();
+}
+
+/** Align control points on the specified axis. */
+void ControlPointSelection::align(Geom::Dim2 axis)
+{
+ enum AlignTargetNode { LAST_NODE=0, FIRST_NODE, MID_NODE, MIN_NODE, MAX_NODE };
+ if (empty()) return;
+ Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+
+ Geom::OptInterval bound;
+ for (auto _point : _points) {
+ bound.unionWith(Geom::OptInterval(_point->position()[d]));
+ }
+
+ if (!bound) { return; }
+
+ double new_coord;
+ switch (AlignTargetNode(prefs->getInt("/dialogs/align/align-nodes-to", 2))){
+ case FIRST_NODE:
+ new_coord=(_points_list.front())->position()[d];
+ break;
+ case LAST_NODE:
+ new_coord=(_points_list.back())->position()[d];
+ break;
+ case MID_NODE:
+ new_coord=bound->middle();
+ break;
+ case MIN_NODE:
+ new_coord=bound->min();
+ break;
+ case MAX_NODE:
+ new_coord=bound->max();
+ break;
+ default:
+ return;
+ }
+
+ for (auto _point : _points) {
+ Geom::Point pos = _point->position();
+ pos[d] = new_coord;
+ _point->move(pos);
+ }
+}
+
+/** Equdistantly distribute control points by moving them in the specified dimension. */
+void ControlPointSelection::distribute(Geom::Dim2 d)
+{
+ if (empty()) return;
+
+ // this needs to be a multimap, otherwise it will fail when some points have the same coord
+ typedef std::multimap<double, SelectableControlPoint*> SortMap;
+
+ SortMap sm;
+ Geom::OptInterval bound;
+ // first we insert all points into a multimap keyed by the aligned coord to sort them
+ // simultaneously we compute the extent of selection
+ for (auto _point : _points) {
+ Geom::Point pos = _point->position();
+ sm.insert(std::make_pair(pos[d], _point));
+ bound.unionWith(Geom::OptInterval(pos[d]));
+ }
+
+ if (!bound) { return; }
+
+ // now we iterate over the multimap and set aligned positions.
+ double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
+ double start = bound->min();
+ unsigned num = 0;
+ for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
+ Geom::Point pos = i->second->position();
+ pos[d] = start + num * step;
+ i->second->move(pos);
+ }
+}
+
+/** Get the bounds of the selection.
+ * @return Smallest rectangle containing the positions of all selected points,
+ * or nothing if the selection is empty */
+Geom::OptRect ControlPointSelection::pointwiseBounds()
+{
+ return _bounds;
+}
+
+Geom::OptRect ControlPointSelection::bounds()
+{
+ return size() == 1 ? (*_points.begin())->bounds() : _bounds;
+}
+
+void ControlPointSelection::showTransformHandles(bool v, bool one_node)
+{
+ _one_node_handles = one_node;
+ _handles_visible = v;
+ _updateTransformHandles(false);
+}
+
+void ControlPointSelection::hideTransformHandles()
+{
+ _handles->setVisible(false);
+}
+void ControlPointSelection::restoreTransformHandles()
+{
+ _updateTransformHandles(true);
+}
+
+void ControlPointSelection::toggleTransformHandlesMode()
+{
+ if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
+ _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
+ if (size() == 1) {
+ _handles->rotationCenter().setVisible(false);
+ }
+ } else {
+ _handles->setMode(TransformHandleSet::MODE_SCALE);
+ }
+}
+
+void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point)
+{
+ hideTransformHandles();
+ _dragging = true;
+ _grabbed_point = point;
+ _farthest_point = point;
+ double maxdist = 0;
+ Geom::Affine m;
+ m.setIdentity();
+ for (auto _point : _points) {
+ _original_positions.insert(std::make_pair(_point, _point->position()));
+ _last_trans.insert(std::make_pair(_point, m));
+ double dist = Geom::distance(*_grabbed_point, *_point);
+ if (dist > maxdist) {
+ maxdist = dist;
+ _farthest_point = _point;
+ }
+ }
+}
+
+void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point];
+ double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]);
+ if (held_only_alt(*event) && fdist > 0) {
+ // Sculpting
+ for (auto cur : _points) {
+ Geom::Affine trans;
+ trans.setIdentity();
+ double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]);
+ double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist);
+ if (dist != 0.0) {
+ // The sculpting transformation is not affine, but it can be
+ // locally approximated by one. Here we compute the local
+ // affine approximation of the sculpting transformation near
+ // the currently transformed point. We then transform the point
+ // by this approximation. This gives us sensible behavior for node handles.
+ // NOTE: probably it would be better to transform the node handles,
+ // but ControlPointSelection is supposed to work for any
+ // SelectableControlPoints, not only Nodes. We could create a specialized
+ // NodeSelection class that inherits from this one and move sculpting there.
+ Geom::Point origdx(Geom::EPSILON, 0);
+ Geom::Point origdy(0, Geom::EPSILON);
+ Geom::Point origp = _original_positions[cur];
+ Geom::Point origpx = _original_positions[cur] + origdx;
+ Geom::Point origpy = _original_positions[cur] + origdy;
+ double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]);
+ double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]);
+ double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist);
+ double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist);
+ Geom::Point newp = origp + abs_delta * deltafrac;
+ Geom::Point newpx = origpx + abs_delta * deltafracdx;
+ Geom::Point newpy = origpy + abs_delta * deltafracdy;
+ Geom::Point newdx = (newpx - newp) / Geom::EPSILON;
+ Geom::Point newdy = (newpy - newp) / Geom::EPSILON;
+
+ Geom::Affine itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0);
+ if (itrans.isSingular())
+ itrans.setIdentity();
+
+ trans *= Geom::Translate(-cur->position());
+ trans *= _last_trans[cur].inverse();
+ trans *= itrans;
+ trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac);
+ _last_trans[cur] = itrans;
+ } else {
+ trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac);
+ }
+ cur->transform(trans);
+ //cur->move(_original_positions[cur] + abs_delta * deltafrac);
+ }
+ } else {
+ Geom::Point delta = new_pos - _grabbed_point->position();
+ for (auto cur : _points) {
+ cur->move(_original_positions[cur] + abs_delta);
+ }
+ _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
+ }
+ signal_update.emit();
+}
+
+void ControlPointSelection::_pointUngrabbed()
+{
+ _original_positions.clear();
+ _last_trans.clear();
+ _dragging = false;
+ _grabbed_point = _farthest_point = nullptr;
+ _updateBounds();
+ restoreTransformHandles();
+ signal_commit.emit(COMMIT_MOUSE_MOVE);
+}
+
+bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
+{
+ // clicking a selected node should toggle the transform handles between rotate and scale mode,
+ // if they are visible
+ if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
+ toggleTransformHandlesMode();
+ return true;
+ }
+ return false;
+}
+
+void ControlPointSelection::_mouseoverChanged()
+{
+ _mouseover_rot_radius = boost::none;
+}
+
+void ControlPointSelection::_update()
+{
+ _updateBounds();
+ _updateTransformHandles(false);
+ if (_bounds) {
+ _handles->rotationCenter().move(_bounds->midpoint());
+ }
+}
+
+void ControlPointSelection::_updateBounds()
+{
+ _rot_radius = boost::none;
+ _bounds = Geom::OptRect();
+ for (auto cur : _points) {
+ Geom::Point p = cur->position();
+ if (!_bounds) {
+ _bounds = Geom::Rect(p, p);
+ } else {
+ _bounds->expandTo(p);
+ }
+ }
+}
+
+void ControlPointSelection::_updateTransformHandles(bool preserve_center)
+{
+ if (_dragging) return;
+
+ if (_handles_visible && size() > 1) {
+ _handles->setBounds(*bounds(), preserve_center);
+ _handles->setVisible(true);
+ } else if (_one_node_handles && size() == 1) { // only one control point in selection
+ SelectableControlPoint *p = *begin();
+ _handles->setBounds(p->bounds());
+ _handles->rotationCenter().move(p->position());
+ _handles->rotationCenter().setVisible(false);
+ _handles->setVisible(true);
+ } else {
+ _handles->setVisible(false);
+ }
+}
+
+/** Moves the selected points along the supplied unit vector according to
+ * the modifier state of the supplied event. */
+bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
+{
+ if (held_control(event)) return false;
+ unsigned num = 1 + combine_key_events(shortcut_key(event), 0);
+
+ Geom::Point delta = dir * num;
+ if (held_shift(event)) delta *= 10;
+ if (held_alt(event)) {
+ delta /= _desktop->current_zoom();
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px");
+ delta *= nudge;
+ }
+
+ transform(Geom::Translate(delta));
+ if (fabs(dir[Geom::X]) > 0) {
+ signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
+ } else {
+ signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
+ }
+ return true;
+}
+
+/**
+ * Computes the distance to the farthest corner of the bounding box.
+ * Used to determine what it means to "rotate by one pixel".
+ */
+double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
+{
+ if (empty()) return 1.0; // some safe value
+ Geom::Rect b = *bounds();
+ double maxlen = 0;
+ for (unsigned i = 0; i < 4; ++i) {
+ double len = Geom::distance(b.corner(i), rc);
+ if (len > maxlen) maxlen = len;
+ }
+ return maxlen;
+}
+
+/**
+ * Rotates the selected points in the given direction according to the modifier state
+ * from the supplied event.
+ * @param event Key event to take modifier state from
+ * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
+ */
+bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
+{
+ if (empty()) return false;
+
+ Geom::Point rc;
+
+ // rotate around the mouseovered point, or the selection's rotation center
+ // if nothing is mouseovered
+ double radius;
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ if (scp) {
+ rc = scp->position();
+ if (!_mouseover_rot_radius) {
+ _mouseover_rot_radius = _rotationRadius(rc);
+ }
+ radius = *_mouseover_rot_radius;
+ } else {
+ rc = _handles->rotationCenter();
+ if (!_rot_radius) {
+ _rot_radius = _rotationRadius(rc);
+ }
+ radius = *_rot_radius;
+ }
+
+ double angle;
+ if (held_alt(event)) {
+ // Rotate by "one pixel". We interpret this as rotating by an angle that causes
+ // the topmost point of a circle circumscribed about the selection's bounding box
+ // to move on an arc 1 screen pixel long.
+ angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ angle = M_PI * dir / snaps;
+ }
+
+ // translate to origin, rotate, translate back to original position
+ Geom::Affine m = Geom::Translate(-rc)
+ * Geom::Rotate(angle) * Geom::Translate(rc);
+ transform(m);
+ signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
+ return true;
+}
+
+
+bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
+{
+ if (empty()) return false;
+
+ double maxext = bounds()->maxExtent();
+ if (Geom::are_near(maxext, 0)) return false;
+
+ Geom::Point center;
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ if (scp) {
+ center = scp->position();
+ } else {
+ center = _handles->rotationCenter().position();
+ }
+
+ double length_change;
+ if (held_alt(event)) {
+ // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
+ // of the bounding box.
+ length_change = 1.0 / _desktop->current_zoom() * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
+ length_change *= dir;
+ }
+ double scale = (maxext + length_change) / maxext;
+
+ Geom::Affine m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
+ transform(m);
+ signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
+ return true;
+}
+
+bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
+{
+ if (empty()) return false;
+
+ Geom::Scale scale_transform(1, 1);
+ if (d == Geom::X) {
+ scale_transform = Geom::Scale(-1, 1);
+ } else {
+ scale_transform = Geom::Scale(1, -1);
+ }
+
+ SelectableControlPoint *scp =
+ dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+ Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
+
+ Geom::Affine m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
+ transform(m);
+ signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
+ return true;
+}
+
+void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
+{
+ _updateBounds();
+ _updateTransformHandles(true);
+ signal_commit.emit(ce);
+}
+
+bool ControlPointSelection::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event)
+{
+ // implement generic event handling that should apply for all control point selections here;
+ // for example, keyboard moves and transformations. This way this functionality doesn't need
+ // to be duplicated in many places
+ // Later split out so that it can be reused in object selection
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ // do not handle key events if the selection is empty
+ if (empty()) break;
+
+ switch(shortcut_key(event->key)) {
+ // moves
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ return _keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir()));
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ return _keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir()));
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ return _keyboardMove(event->key, Geom::Point(1, 0));
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ return _keyboardMove(event->key, Geom::Point(-1, 0));
+
+ // rotates
+ case GDK_KEY_bracketleft:
+ return _keyboardRotate(event->key, -_desktop->yaxisdir());
+ case GDK_KEY_bracketright:
+ return _keyboardRotate(event->key, _desktop->yaxisdir());
+
+ // scaling
+ case GDK_KEY_less:
+ case GDK_KEY_comma:
+ return _keyboardScale(event->key, -1);
+ case GDK_KEY_greater:
+ case GDK_KEY_period:
+ return _keyboardScale(event->key, 1);
+
+ // TODO: skewing
+
+ // flipping
+ // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
+ case GDK_KEY_h:
+ case GDK_KEY_H:
+ if (held_shift(event->key)) {
+ toggleTransformHandlesMode();
+ return true;
+ }
+ // any modifiers except shift should cause no action
+ if (held_any_modifiers(event->key)) break;
+ return _keyboardFlip(Geom::X);
+ case GDK_KEY_v:
+ case GDK_KEY_V:
+ if (held_any_modifiers(event->key)) break;
+ return _keyboardFlip(Geom::Y);
+ default: break;
+ }
+ break;
+ default: break;
+ }
+ return false;
+}
+
+void ControlPointSelection::getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
+{
+ pts.clear();
+ for (auto _point : _points) {
+ pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE);
+ }
+}
+
+void ControlPointSelection::getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
+{
+ pts.clear();
+ ControlPointSelection::Set &nodes = this->allPoints();
+ for (auto node : nodes) {
+ if (!node->selected()) {
+ Node *n = static_cast<Node*>(node);
+ pts.push_back(n->snapCandidatePoint());
+ }
+ }
+}
+
+void ControlPointSelection::setOriginalPoints()
+{
+ _original_positions.clear();
+ for (auto _point : _points) {
+ _original_positions.insert(std::make_pair(_point, _point->position()));
+ }
+}
+
+} // 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/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h
new file mode 100644
index 0000000..b50f1f2
--- /dev/null
+++ b/src/ui/tool/control-point-selection.h
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Control point selection - stores a set of control points and applies transformations
+ * to them
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H
+#define SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H
+
+#include <list>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <boost/optional.hpp>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <2geom/forward.h>
+#include <2geom/point.h>
+#include <2geom/rect.h>
+#include "ui/tool/commit-events.h"
+#include "ui/tool/manipulator.h"
+#include "snap-candidate.h"
+
+class SPDesktop;
+struct SPCanvasGroup;
+
+namespace Inkscape {
+namespace UI {
+class TransformHandleSet;
+class SelectableControlPoint;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+class ControlPointSelection : public Manipulator, public sigc::trackable {
+public:
+ ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group);
+ ~ControlPointSelection() override;
+ typedef std::unordered_set<SelectableControlPoint *> set_type;
+ typedef set_type Set; // convenience alias
+
+ typedef set_type::iterator iterator;
+ typedef set_type::const_iterator const_iterator;
+ typedef set_type::size_type size_type;
+ typedef SelectableControlPoint *value_type;
+ typedef SelectableControlPoint *key_type;
+
+ // size
+ bool empty() { return _points.empty(); }
+ size_type size() { return _points.size(); }
+
+ // iterators
+ iterator begin() { return _points.begin(); }
+ const_iterator begin() const { return _points.begin(); }
+ iterator end() { return _points.end(); }
+ const_iterator end() const { return _points.end(); }
+
+ // insert
+ std::pair<iterator, bool> insert(const value_type& x, bool notify = true, bool to_update = true);
+ template <class InputIterator>
+ void insert(InputIterator first, InputIterator last) {
+ for (; first != last; ++first) {
+ insert(*first, false, false);
+ }
+ _update();
+ signal_selection_changed.emit(std::vector<key_type>(first, last), true);
+ }
+
+ // erase
+ void clear();
+ void erase(iterator pos, bool to_update = true);
+ size_type erase(const key_type& k, bool notify = true);
+ void erase(iterator first, iterator last);
+
+ // find
+ iterator find(const key_type &k) {
+ return _points.find(k);
+ }
+
+ // Sometimes it is very useful to keep a list of all selectable points.
+ set_type const &allPoints() const { return _all_points; }
+ set_type &allPoints() { return _all_points; }
+ // ...for example in these methods. Another useful case is snapping.
+ void selectAll();
+ void selectArea(Geom::Rect const &);
+ void invertSelection();
+ void spatialGrow(SelectableControlPoint *origin, int dir);
+
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ void transform(Geom::Affine const &m);
+ void align(Geom::Dim2 d);
+ void distribute(Geom::Dim2 d);
+
+ Geom::OptRect pointwiseBounds();
+ Geom::OptRect bounds();
+
+ bool transformHandlesEnabled() { return _handles_visible; }
+ void showTransformHandles(bool v, bool one_node);
+ // the two methods below do not modify the state; they are for use in manipulators
+ // that need to temporarily hide the handles, for example when moving a node
+ void hideTransformHandles();
+ void restoreTransformHandles();
+ void toggleTransformHandlesMode();
+
+ sigc::signal<void> signal_update;
+ // It turns out that emitting a signal after every point is selected or deselected is not too efficient,
+ // so this can be done in a massive group once the selection is finally changed.
+ sigc::signal<void, std::vector<SelectableControlPoint *>, bool> signal_selection_changed;
+ sigc::signal<void, CommitEvent> signal_commit;
+
+ void getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts);
+ void getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts);
+ void setOriginalPoints();
+ //the purpose of this list is to keep track of first and last selected
+ std::list<SelectableControlPoint *> _points_list;
+
+private:
+ // The functions below are invoked from SelectableControlPoint.
+ // Previously they were connected to handlers when selecting, but this
+ // creates problems when dragging a point that was not selected.
+ void _pointGrabbed(SelectableControlPoint *);
+ void _pointDragged(Geom::Point &, GdkEventMotion *);
+ void _pointUngrabbed();
+ bool _pointClicked(SelectableControlPoint *, GdkEventButton *);
+ void _mouseoverChanged();
+
+ void _update();
+ void _updateTransformHandles(bool preserve_center);
+ void _updateBounds();
+ bool _keyboardMove(GdkEventKey const &, Geom::Point const &);
+ bool _keyboardRotate(GdkEventKey const &, int);
+ bool _keyboardScale(GdkEventKey const &, int);
+ bool _keyboardFlip(Geom::Dim2);
+ void _keyboardTransform(Geom::Affine const &);
+ void _commitHandlesTransform(CommitEvent ce);
+ double _rotationRadius(Geom::Point const &);
+
+ set_type _points;
+
+ set_type _all_points;
+ std::unordered_map<SelectableControlPoint *, Geom::Point> _original_positions;
+ std::unordered_map<SelectableControlPoint *, Geom::Affine> _last_trans;
+ boost::optional<double> _rot_radius;
+ boost::optional<double> _mouseover_rot_radius;
+ Geom::OptRect _bounds;
+ TransformHandleSet *_handles;
+ SelectableControlPoint *_grabbed_point, *_farthest_point;
+ unsigned _dragging : 1;
+ unsigned _handles_visible : 1;
+ unsigned _one_node_handles : 1;
+
+ friend class SelectableControlPoint;
+};
+
+} // 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 :
diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp
new file mode 100644
index 0000000..c6f18f2
--- /dev/null
+++ b/src/ui/tool/control-point.cpp
@@ -0,0 +1,637 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm.h>
+
+#include <2geom/point.h>
+
+#include "desktop.h"
+#include "message-context.h"
+
+#include "display/sp-canvas.h"
+#include "display/snap-indicator.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/tools/tool-base.h"
+#include "ui/control-manager.h"
+#include "ui/tool/control-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/transform-handle-set.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+// Default colors for control points
+ControlPoint::ColorSet ControlPoint::_default_color_set = {
+ {0xffffff00, 0x01000000}, // normal fill, stroke
+ {0xff0000ff, 0x01000000}, // mouseover fill, stroke
+ {0x0000ffff, 0x01000000}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+ControlPoint *ControlPoint::mouseovered_point = nullptr;
+
+sigc::signal<void, ControlPoint*> ControlPoint::signal_mouseover_change;
+
+Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity());
+
+Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity());
+
+int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK |
+ GDK_KEY_RELEASE_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+
+bool ControlPoint::_drag_initiated = false;
+bool ControlPoint::_event_grab = false;
+
+ControlPoint::ColorSet ControlPoint::invisible_cset = {
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000},
+ {0x00000000, 0x00000000}
+};
+
+ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ColorSet const &cset, SPCanvasGroup *group) :
+ _desktop(d),
+ _canvas_item(nullptr),
+ _cset(cset),
+ _state(STATE_NORMAL),
+ _position(initial_pos),
+ _lurking(false),
+ _double_clicked(false)
+{
+ _canvas_item = sp_canvas_item_new(
+ group ? group : _desktop->getControls(), SP_TYPE_CTRL,
+ "anchor", (SPAnchorType) anchor, "size", (unsigned int) pixbuf->get_width(),
+ "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(),
+ "filled", TRUE, "fill_color", _cset.normal.fill,
+ "stroked", TRUE, "stroke_color", _cset.normal.stroke,
+ "mode", SP_CTRL_MODE_XOR, NULL);
+
+ _commonInit();
+}
+
+ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ ControlType type,
+ ColorSet const &cset, SPCanvasGroup *group) :
+ _desktop(d),
+ _canvas_item(nullptr),
+ _cset(cset),
+ _state(STATE_NORMAL),
+ _position(initial_pos),
+ _lurking(false),
+ _double_clicked(false)
+{
+ _canvas_item = ControlManager::getManager().createControl(group ? group : _desktop->getControls(), type);
+ g_object_set(_canvas_item,
+ "anchor", anchor,
+ "filled", TRUE, "fill_color", _cset.normal.fill,
+ "stroked", TRUE, "stroke_color", _cset.normal.stroke,
+ "mode", SP_CTRL_MODE_XOR, NULL);
+ _commonInit();
+}
+
+ControlPoint::~ControlPoint()
+{
+ // avoid storing invalid points in mouseovered_point
+ if (this == mouseovered_point) {
+ _clearMouseover();
+ }
+
+ g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection);
+ //sp_canvas_item_hide(_canvas_item);
+ sp_canvas_item_destroy(_canvas_item);
+}
+
+void ControlPoint::_commonInit()
+{
+ SP_CTRL(_canvas_item)->moveto(_position);
+ _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event",
+ G_CALLBACK(_event_handler), this);
+}
+
+void ControlPoint::setPosition(Geom::Point const &pos)
+{
+ _position = pos;
+ SP_CTRL(_canvas_item)->moveto(pos);
+}
+
+void ControlPoint::move(Geom::Point const &pos)
+{
+ setPosition(pos);
+}
+
+void ControlPoint::transform(Geom::Affine const &m) {
+ move(position() * m);
+}
+
+bool ControlPoint::visible() const
+{
+ return sp_canvas_item_is_visible(_canvas_item);
+}
+
+void ControlPoint::setVisible(bool v)
+{
+ if (v) sp_canvas_item_show(_canvas_item);
+ else sp_canvas_item_hide(_canvas_item);
+}
+
+Glib::ustring ControlPoint::format_tip(char const *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ char *dyntip = g_strdup_vprintf(format, args);
+ va_end(args);
+ Glib::ustring ret = dyntip;
+ g_free(dyntip);
+ return ret;
+}
+
+unsigned int ControlPoint::_size() const
+{
+ unsigned int ret;
+ g_object_get(_canvas_item, "size", &ret, NULL);
+ return ret;
+}
+
+SPCtrlShapeType ControlPoint::_shape() const
+{
+ SPCtrlShapeType ret;
+ g_object_get(_canvas_item, "shape", &ret, NULL);
+ return ret;
+}
+
+SPAnchorType ControlPoint::_anchor() const
+{
+ SPAnchorType ret;
+ g_object_get(_canvas_item, "anchor", &ret, NULL);
+ return ret;
+}
+
+Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf()
+{
+ GdkPixbuf *ret;
+ g_object_get(_canvas_item, "pixbuf", &ret, NULL);
+ return Glib::wrap(ret);
+}
+
+// Same for setters.
+
+void ControlPoint::_setSize(unsigned int size)
+{
+ g_object_set(_canvas_item, "size", size, NULL);
+}
+
+bool ControlPoint::_setControlType(Inkscape::ControlType type)
+{
+ return ControlManager::getManager().setControlType(_canvas_item, type);
+}
+
+void ControlPoint::_setAnchor(SPAnchorType anchor)
+{
+ g_object_set(_canvas_item, "anchor", anchor, NULL);
+}
+
+void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p)
+{
+ g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL);
+}
+
+// re-routes events into the virtual function
+int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point)
+{
+ if ((point == nullptr) || (point->_desktop == nullptr)) {
+ return FALSE;
+ }
+ return point->_eventHandler(point->_desktop->event_context, event) ? TRUE : FALSE;
+}
+
+// main event callback, which emits all other callbacks.
+bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // NOTE the static variables below are shared for all points!
+ // TODO handle clicks and drags from other buttons too
+
+ if (event == nullptr)
+ {
+ return false;
+ }
+
+ if (event_context == nullptr)
+ {
+ return false;
+ }
+ if (_desktop == nullptr)
+ {
+ return false;
+ }
+ if(event_context->desktop !=_desktop)
+ {
+ g_warning ("ControlPoint: desktop pointers not equal!");
+ //return false;
+ }
+ // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
+ static Geom::Point pointer_offset;
+ // number of last doubleclicked button
+ static unsigned next_release_doubleclick = 0;
+ _double_clicked = false;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ GdkEventMotion em;
+ SPCanvas* Ca;
+ switch(event->type)
+ {
+ case GDK_BUTTON_PRESS:
+ next_release_doubleclick = 0;
+ if (event->button.button == 1 && !event_context->space_panning) {
+ // 1st mouse button click. internally, start dragging, but do not emit signals
+ // or change position until drag tolerance is exceeded.
+ _drag_event_origin[Geom::X] = event->button.x;
+ _drag_event_origin[Geom::Y] = event->button.y;
+ pointer_offset = _position - _desktop->w2d(_drag_event_origin);
+ _drag_initiated = false;
+ // route all events to this handler
+ sp_canvas_item_grab(_canvas_item, _grab_event_mask, nullptr, event->button.time);
+ _event_grab = true;
+ _setState(STATE_CLICKED);
+ return true;
+ }
+ return _event_grab;
+
+ case GDK_2BUTTON_PRESS:
+ // store the button number for next release
+ next_release_doubleclick = event->button.button;
+ return true;
+
+ case GDK_MOTION_NOTIFY:
+ Ca = _desktop->canvas;
+ em = event->motion;
+ combine_motion_events(Ca, em, 0);
+
+ if (_event_grab && ! event_context->space_panning) {
+ _desktop->snapindicator->remove_snaptarget();
+ bool transferred = false;
+ if (!_drag_initiated) {
+ bool t = fabs(em.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
+ fabs(em.y - _drag_event_origin[Geom::Y]) <= drag_tolerance;
+ if (t){
+ return true;
+ }
+
+ // if we are here, it means the tolerance was just exceeded.
+ _drag_origin = _position;
+ transferred = grabbed(&em);
+ // _drag_initiated might change during the above virtual call
+ if (!_drag_initiated) {
+ // this guarantees smooth redraws while dragging
+ _desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ _drag_initiated = true;
+ }
+ }
+
+ if (!transferred) {
+ // dragging in progress
+ Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset;
+ // the new position is passed by reference and can be changed in the handlers.
+ dragged(new_pos, &em);
+ move(new_pos);
+ _updateDragTip(&em); // update dragging tip after moving to new position
+
+ _desktop->scroll_to_point(new_pos);
+ _desktop->set_coordinate_status(_position);
+ sp_event_context_snap_delay_handler(event_context, nullptr,
+ (gpointer) this, &event->motion,
+ Inkscape::UI::Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER);
+ }
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (_event_grab && event->button.button == 1) {
+ // If we have any pending snap event, then invoke it now!
+ // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event
+ // if the mouse speed was too high. This is inherent to the snap-delay mechanism.
+ // We must snap at some point in time though, and this is our last chance)
+ // PS: For other contexts this is handled already in sp_event_context_item_handler or
+ // sp_event_context_root_handler
+ //if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) {
+ if (event_context->_delayed_snap_event) {
+ sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event);
+ }
+
+ sp_canvas_item_ungrab(_canvas_item);
+ _setMouseover(this, event->button.state);
+ _event_grab = false;
+
+ if (_drag_initiated) {
+ // it is the end of a drag
+ _desktop->canvas->endForcedFullRedraws();
+ _drag_initiated = false;
+ ungrabbed(&event->button);
+ return true;
+ } else {
+ // it is the end of a click
+ if (next_release_doubleclick) {
+ _double_clicked = true;
+ return doubleclicked(&event->button);
+ } else {
+ return clicked(&event->button);
+ }
+ }
+ }
+ break;
+
+ case GDK_ENTER_NOTIFY:
+ _setMouseover(this, event->crossing.state);
+ return true;
+ case GDK_LEAVE_NOTIFY:
+ _clearMouseover();
+ return true;
+
+ case GDK_GRAB_BROKEN:
+ if (_event_grab && !event->grab_broken.keyboard) {
+ {
+ ungrabbed(nullptr);
+ if (_drag_initiated) {
+ _desktop->canvas->endForcedFullRedraws();
+ }
+ }
+ _setState(STATE_NORMAL);
+ _event_grab = false;
+ _drag_initiated = false;
+ return true;
+ }
+ break;
+
+ // update tips on modifier state change
+ // TODO add ESC keybinding as drag cancel
+ case GDK_KEY_PRESS:
+ switch (Inkscape::UI::Tools::get_latin_keyval(&event->key))
+ {
+ case GDK_KEY_Escape: {
+ // ignore Escape if this is not a drag
+ if (!_drag_initiated) break;
+
+ // temporarily disable snapping - we might snap to a different place than we were initially
+ sp_event_context_discard_delayed_snap_event(event_context);
+ SnapPreferences &snapprefs = _desktop->namedview->snap_manager.snapprefs;
+ bool snap_save = snapprefs.getSnapEnabledGlobally();
+ snapprefs.setSnapEnabledGlobally(false);
+
+ Geom::Point new_pos = _drag_origin;
+
+ // make a fake event for dragging
+ // ASSUMPTION: dragging a point without modifiers will never prevent us from moving it
+ // to its original position
+ GdkEventMotion fake;
+ fake.type = GDK_MOTION_NOTIFY;
+ fake.window = event->key.window;
+ fake.send_event = event->key.send_event;
+ fake.time = event->key.time;
+ fake.x = _drag_event_origin[Geom::X]; // these two are normally not used in handlers
+ fake.y = _drag_event_origin[Geom::Y]; // (and shouldn't be)
+ fake.axes = nullptr;
+ fake.state = 0; // unconstrained drag
+ fake.is_hint = FALSE;
+ fake.device = nullptr;
+ fake.x_root = -1; // not used in handlers (and shouldn't be)
+ fake.y_root = -1; // can be used as a flag to check for cancelled drag
+
+ dragged(new_pos, &fake);
+
+ sp_canvas_item_ungrab(_canvas_item);
+ _clearMouseover(); // this will also reset state to normal
+ _desktop->canvas->endForcedFullRedraws();
+ _event_grab = false;
+ _drag_initiated = false;
+
+ ungrabbed(nullptr); // ungrabbed handlers can handle a NULL event
+ snapprefs.setSnapEnabledGlobally(snap_save);
+ }
+ return true;
+ case GDK_KEY_Tab:
+ {// Downcast from ControlPoint to TransformHandle, if possible
+ // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
+ TransformHandle *th = dynamic_cast<TransformHandle*>(this);
+ if (th) {
+ th->getNextClosestPoint(false);
+ return true;
+ }
+ break;
+ }
+ case GDK_KEY_ISO_Left_Tab:
+ {// Downcast from ControlPoint to TransformHandle, if possible
+ // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
+ TransformHandle *th = dynamic_cast<TransformHandle*>(this);
+ if (th) {
+ th->getNextClosestPoint(true);
+ return true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ // Do not break here, to allow for updating tooltips and such
+ case GDK_KEY_RELEASE:
+ if (mouseovered_point != this){
+ return false;
+ }
+ if (_drag_initiated) {
+ return true; // this prevents the tool from overwriting the drag tip
+ } else {
+ unsigned state = state_after_event(event);
+ if (state != event->key.state) {
+ // we need to return true if there was a tip available, otherwise the tool's
+ // handler will process this event and set the tool's message, overwriting
+ // the point's message
+ return _updateTip(state);
+ }
+ }
+ break;
+
+ default: break;
+ }
+
+ // do not propagate events during grab - it might cause problems
+ return _event_grab;
+}
+
+void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
+{
+ bool visible = p->visible();
+ if (visible) { // invisible points shouldn't get mouseovered
+ p->_setState(STATE_MOUSEOVER);
+ }
+ p->_updateTip(state);
+
+ if (visible && mouseovered_point != p) {
+ mouseovered_point = p;
+ signal_mouseover_change.emit(mouseovered_point);
+ }
+}
+
+bool ControlPoint::_updateTip(unsigned state)
+{
+ Glib::ustring tip = _getTip(state);
+ if (!tip.empty()) {
+ _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
+ tip.data());
+ return true;
+ } else {
+ _desktop->event_context->defaultMessageContext()->clear();
+ return false;
+ }
+}
+
+bool ControlPoint::_updateDragTip(GdkEventMotion *event)
+{
+ if (!_hasDragTips()) {
+ return false;
+ }
+ Glib::ustring tip = _getDragTip(event);
+ if (!tip.empty()) {
+ _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
+ tip.data());
+ return true;
+ } else {
+ _desktop->event_context->defaultMessageContext()->clear();
+ return false;
+ }
+}
+
+void ControlPoint::_clearMouseover()
+{
+ if (mouseovered_point) {
+ mouseovered_point->_desktop->event_context->defaultMessageContext()->clear();
+ mouseovered_point->_setState(STATE_NORMAL);
+ mouseovered_point = nullptr;
+ signal_mouseover_change.emit(mouseovered_point);
+ }
+}
+
+void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event)
+{
+ if (!_event_grab) return;
+
+ grabbed(event);
+ sp_canvas_item_ungrab(prev_point->_canvas_item);
+ sp_canvas_item_grab(_canvas_item, _grab_event_mask, nullptr, event->time);
+
+ if (!_drag_initiated) {
+ _desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ _drag_initiated = true;
+ }
+
+ prev_point->_setState(STATE_NORMAL);
+ _setMouseover(this, event->state);
+}
+
+void ControlPoint::_setState(State state)
+{
+ ColorEntry current = {0, 0};
+ ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset;
+ switch(state) {
+ case STATE_NORMAL:
+ current = activeCset.normal;
+ break;
+ case STATE_MOUSEOVER:
+ current = activeCset.mouseover;
+ break;
+ case STATE_CLICKED:
+ current = activeCset.clicked;
+ break;
+ };
+ _setColors(current);
+ _state = state;
+}
+
+void ControlPoint::_handleControlStyling()
+{
+ if (_canvas_item->ctrlType != CTRL_TYPE_UNKNOWN) {
+ ControlManager::getManager().updateItem(_canvas_item);
+ }
+}
+
+void ControlPoint::_setColors(ColorEntry colors)
+{
+ g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL);
+}
+
+bool ControlPoint::_isLurking()
+{
+ return _lurking;
+}
+
+void ControlPoint::_setLurking(bool lurking)
+{
+ if (lurking != _lurking) {
+ _lurking = lurking;
+ _setState(_state); // TODO refactor out common part
+ }
+}
+
+
+bool ControlPoint::_is_drag_cancelled(GdkEventMotion *event)
+{
+ return !event || event->x_root == -1;
+}
+
+// dummy implementations for handlers
+
+bool ControlPoint::grabbed(GdkEventMotion * /*event*/)
+{
+ return false;
+}
+
+void ControlPoint::dragged(Geom::Point &/*new_pos*/, GdkEventMotion * /*event*/)
+{
+}
+
+void ControlPoint::ungrabbed(GdkEventButton * /*event*/)
+{
+}
+
+bool ControlPoint::clicked(GdkEventButton * /*event*/)
+{
+ return false;
+}
+
+bool ControlPoint::doubleclicked(GdkEventButton * /*event*/)
+{
+ return false;
+}
+
+} // 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/tool/control-point.h b/src/ui/tool/control-point.h
new file mode 100644
index 0000000..b97f3cb
--- /dev/null
+++ b/src/ui/tool/control-point.h
@@ -0,0 +1,411 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2012 Authors
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CONTROL_POINT_H
+#define SEEN_UI_TOOL_CONTROL_POINT_H
+
+#include <gdkmm/pixbuf.h>
+#include <boost/utility.hpp>
+#include <cstddef>
+#include <sigc++/signal.h>
+#include <sigc++/trackable.h>
+#include <2geom/point.h>
+
+#include "ui/control-types.h"
+#include "display/sodipodi-ctrl.h"
+#include "enums.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ToolBase;
+
+}
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * Draggable point, the workhorse of on-canvas editing.
+ *
+ * Control points (formerly known as knots) are graphical representations of some significant
+ * point in the drawing. The drawing can be changed by dragging the point and the things that are
+ * attached to it with the mouse. Example things that could be edited with draggable points
+ * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles
+ * in a path, and many more.
+ *
+ * @par Control point event handlers
+ * @par
+ * The control point has several virtual methods which allow you to react to things that
+ * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions.
+ * When a drag happens, the order of calls is as follows:
+ * - <tt>grabbed()</tt>
+ * - <tt>dragged()</tt>
+ * - <tt>dragged()</tt>
+ * - <tt>dragged()</tt>
+ * - ...
+ * - <tt>dragged()</tt>
+ * - <tt>ungrabbed()</tt>
+ *
+ * The control point can also respond to clicks and double clicks. On a double click,
+ * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint,
+ * you need to manually call the superclass version at the appropriate point in your handler.
+ *
+ * @par Which method to override?
+ * @par
+ * You might wonder which hook to use when you want to do things when the point is relocated.
+ * Here are some tips:
+ * - If the point is used to edit an object, override the move() method.
+ * - If the point can usually be dragged wherever you like but can optionally be constrained
+ * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new
+ * position argument.
+ * - If the point has additional canvas items tied to it (like handle lines), override
+ * the setPosition() method.
+ */
+class ControlPoint : boost::noncopyable, public sigc::trackable {
+public:
+
+ /**
+ * Enumeration representing the possible states of the control point, used to determine
+ * its appearance.
+ *
+ * @todo resolve this to be in sync with the five standard GTK states.
+ */
+ enum State {
+ /** Normal state. */
+ STATE_NORMAL,
+
+ /** Mouse is hovering over the control point. */
+ STATE_MOUSEOVER,
+
+ /** First mouse button pressed over the control point. */
+ STATE_CLICKED
+ };
+
+ /**
+ * Destructor
+ */
+ virtual ~ControlPoint();
+
+ /// @name Adjust the position of the control point
+ /// @{
+ /** Current position of the control point. */
+ Geom::Point const &position() const { return _position; }
+
+ operator Geom::Point const &() { return _position; }
+
+ /**
+ * Move the control point to new position with side effects.
+ * This is called after each drag. Override this method if only some positions make sense
+ * for a control point (like a point that must always be on a path and can't modify it),
+ * or when moving a control point changes the positions of other points.
+ */
+ virtual void move(Geom::Point const &pos);
+
+ /**
+ * Relocate the control point without side effects.
+ * Overload this method only if there is an additional graphical representation
+ * that must be updated (like the lines that connect handles to nodes). If you override it,
+ * you must also call the superclass implementation of the method.
+ * @todo Investigate whether this method should be protected
+ */
+ virtual void setPosition(Geom::Point const &pos);
+
+ /**
+ * Apply an arbitrary affine transformation to a control point. This is used
+ * by ControlPointSelection, and is important for things like nodes with handles.
+ * The default implementation simply moves the point according to the transform.
+ */
+ virtual void transform(Geom::Affine const &m);
+ /// @}
+
+ /// @name Toggle the point's visibility
+ /// @{
+ bool visible() const;
+
+ /**
+ * Set the visibility of the control point. An invisible point is not drawn on the canvas
+ * and cannot receive any events. If you want to have an invisible point that can respond
+ * to events, use <tt>invisible_cset</tt> as its color set.
+ */
+ virtual void setVisible(bool v);
+ /// @}
+
+ /// @name Transfer grab from another event handler
+ /// @{
+ /**
+ * Transfer the grab to another point. This method allows one to create a draggable point
+ * that should be dragged instead of the one that received the grabbed signal.
+ * This is used to implement dragging out handles in the new node tool, for example.
+ *
+ * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate
+ * using it with selectable control points. If you use this method while dragging, you must emit
+ * the ungrab signal yourself.
+ *
+ * Note that this will break horribly if you try to transfer grab between points in different
+ * desktops, which doesn't make much sense anyway.
+ */
+ void transferGrab(ControlPoint *from, GdkEventMotion *event);
+ /// @}
+
+ /// @name Inspect the state of the control point
+ /// @{
+ State state() const { return _state; }
+
+ bool mouseovered() const { return this == mouseovered_point; }
+ /// @}
+
+ /** Holds the currently mouseovered control point. */
+ static ControlPoint *mouseovered_point;
+
+ /**
+ * Emitted when the mouseovered point changes. The parameter is the new mouseovered point.
+ * When a point ceases to be mouseovered, the parameter will be NULL.
+ */
+ static sigc::signal<void, ControlPoint*> signal_mouseover_change;
+
+ static Glib::ustring format_tip(char const *format, ...) G_GNUC_PRINTF(1,2);
+
+ // temporarily public, until snap delay is refactored a little
+ virtual bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event);
+ SPDesktop *const _desktop; ///< The desktop this control point resides on.
+
+ bool doubleClicked() {return _double_clicked;}
+
+protected:
+
+ struct ColorEntry {
+ guint32 fill;
+ guint32 stroke;
+ };
+
+ /**
+ * Color entries for each possible state.
+ * @todo resolve this to be in sync with the five standard GTK states.
+ */
+ struct ColorSet {
+ ColorEntry normal;
+ ColorEntry mouseover;
+ ColorEntry clicked;
+ ColorEntry selected_normal;
+ ColorEntry selected_mouseover;
+ ColorEntry selected_clicked;
+ };
+
+ /**
+ * A color set which you can use to create an invisible control that can still receive events.
+ */
+ static ColorSet invisible_cset;
+
+ /**
+ * Create a regular control point.
+ * Derive to have constructors with a reasonable number of parameters.
+ *
+ * @param d Desktop for this control
+ * @param initial_pos Initial position of the control point in desktop coordinates
+ * @param anchor Where is the control point rendered relative to its desktop coordinates
+ * @param type Logical type of the control point.
+ * @param cset Colors of the point
+ * @param group The canvas group the point's canvas item should be created in
+ */
+ ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ ControlType type,
+ ColorSet const &cset = _default_color_set, SPCanvasGroup *group = nullptr);
+
+ /**
+ * Create a control point with a pixbuf-based visual representation.
+ *
+ * @param d Desktop for this control
+ * @param initial_pos Initial position of the control point in desktop coordinates
+ * @param anchor Where is the control point rendered relative to its desktop coordinates
+ * @param pixbuf Pixbuf to be used as the visual representation
+ * @param cset Colors of the point
+ * @param group The canvas group the point's canvas item should be created in
+ */
+ ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ColorSet const &cset = _default_color_set, SPCanvasGroup *group = nullptr);
+
+ /// @name Handle control point events in subclasses
+ /// @{
+ /**
+ * Called when the user moves the point beyond the drag tolerance with the first button held
+ * down.
+ *
+ * @param event Motion event when drag tolerance was exceeded.
+ * @return true if you called transferGrab() during this method.
+ */
+ virtual bool grabbed(GdkEventMotion *event);
+
+ /**
+ * Called while dragging, but before moving the knot to new position.
+ *
+ * @param pos Old position, always equal to position()
+ * @param new_pos New position (after drag). This is passed as a non-const reference,
+ * so you can change it from the handler - that's how constrained dragging is implemented.
+ * @param event Motion event.
+ */
+ virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event);
+
+ /**
+ * Called when the control point finishes a drag.
+ *
+ * @param event Button release event
+ */
+ virtual void ungrabbed(GdkEventButton *event);
+
+ /**
+ * Called when the control point is clicked, at mouse button release.
+ * Improperly implementing this method can cause the default context menu not to appear when a control
+ * point is right-clicked.
+ *
+ * @param event Button release event
+ * @return true if the click had some effect, false if it did nothing.
+ */
+ virtual bool clicked(GdkEventButton *event);
+
+ /**
+ * Called when the control point is doubleclicked, at mouse button release.
+ *
+ * @param event Button release event
+ */
+ virtual bool doubleclicked(GdkEventButton *event);
+ /// @}
+
+ /// @name Manipulate the control point's appearance in subclasses
+ /// @{
+
+ /**
+ * Change the state of the knot.
+ * Alters the appearance of the knot to match one of the states: normal, mouseover
+ * or clicked.
+ */
+ virtual void _setState(State state);
+
+ void _handleControlStyling();
+
+ void _setColors(ColorEntry c);
+
+ unsigned int _size() const;
+
+ SPCtrlShapeType _shape() const;
+
+ SPAnchorType _anchor() const;
+
+ Glib::RefPtr<Gdk::Pixbuf> _pixbuf();
+
+ void _setSize(unsigned int size);
+
+ bool _setControlType(Inkscape::ControlType type);
+
+ void _setAnchor(SPAnchorType anchor);
+
+ void _setPixbuf(Glib::RefPtr<Gdk::Pixbuf>);
+
+ /**
+ * Determines if the control point is not visible yet still reacting to events.
+ *
+ * @return true if non-visible, false otherwise.
+ */
+ bool _isLurking();
+
+ /**
+ * Sets the control point to be non-visible yet still reacting to events.
+ *
+ * @param lurking true to make non-visible, false otherwise.
+ */
+ void _setLurking(bool lurking);
+
+ /// @}
+
+ virtual Glib::ustring _getTip(unsigned /*state*/) const { return ""; }
+
+ virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) const { return ""; }
+
+ virtual bool _hasDragTips() const { return false; }
+
+
+ SPCanvasItem * _canvas_item; ///< Visual representation of the control point.
+
+ ColorSet const &_cset; ///< Colors used to represent the point
+
+ State _state;
+
+ static Geom::Point const &_last_click_event_point() { return _drag_event_origin; }
+
+ static Geom::Point const &_last_drag_origin() { return _drag_origin; }
+
+ static bool _is_drag_cancelled(GdkEventMotion *event);
+
+ /** Events which should be captured when a handle is being dragged. */
+ static int const _grab_event_mask;
+
+ static bool _drag_initiated;
+
+private:
+
+ ControlPoint(ControlPoint const &other);
+
+ void operator=(ControlPoint const &other);
+
+ static int _event_handler(SPCanvasItem *item, GdkEvent *event, ControlPoint *point);
+
+ static void _setMouseover(ControlPoint *, unsigned state);
+
+ static void _clearMouseover();
+
+ bool _updateTip(unsigned state);
+
+ bool _updateDragTip(GdkEventMotion *event);
+
+ void _setDefaultColors();
+
+ void _commonInit();
+
+ Geom::Point _position; ///< Current position in desktop coordinates
+
+ gulong _event_handler_connection;
+
+ bool _lurking;
+
+ static ColorSet _default_color_set;
+
+ /** Stores the window point over which the cursor was during the last mouse button press. */
+ static Geom::Point _drag_event_origin;
+
+ /** Stores the desktop point from which the last drag was initiated. */
+ static Geom::Point _drag_origin;
+
+ static bool _event_grab;
+
+ bool _double_clicked;
+};
+
+
+} // 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/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp
new file mode 100644
index 0000000..4e878d0
--- /dev/null
+++ b/src/ui/tool/curve-drag-point.cpp
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tool/curve-drag-point.h"
+#include <glib/gi18n.h>
+#include "desktop.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+bool CurveDragPoint::_drags_stroke = false;
+bool CurveDragPoint::_segment_was_degenerate = false;
+
+CurveDragPoint::CurveDragPoint(PathManipulator &pm) :
+ ControlPoint(pm._multi_path_manipulator._path_data.node_data.desktop, Geom::Point(), SP_ANCHOR_CENTER,
+ CTRL_TYPE_INVISIPOINT,
+ invisible_cset, pm._multi_path_manipulator._path_data.dragpoint_group),
+ _pm(pm)
+{
+ setVisible(false);
+}
+
+bool CurveDragPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // do not process any events when the manipulator is empty
+ if (_pm.empty()) {
+ setVisible(false);
+ return false;
+ }
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+bool CurveDragPoint::grabbed(GdkEventMotion */*event*/)
+{
+ _pm._selection.hideTransformHandles();
+ NodeList::iterator second = first.next();
+
+ // move the handles to 1/3 the length of the segment for line segments
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ _segment_was_degenerate = true;
+
+ // delta is a vector equal 1/3 of distance from first to second
+ Geom::Point delta = (second->position() - first->position()) / 3.0;
+ // only update the nodes if the mode is bspline
+ if(!_pm._isBSpline()){
+ first->front()->move(first->front()->position() + delta);
+ second->back()->move(second->back()->position() - delta);
+ }
+ _pm.update();
+ } else {
+ _segment_was_degenerate = false;
+ }
+ return false;
+}
+
+void CurveDragPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ if (!first || !first.next()) return;
+ NodeList::iterator second = first.next();
+
+ // special cancel handling - retract handles when if the segment was degenerate
+ if (_is_drag_cancelled(event) && _segment_was_degenerate) {
+ first->front()->retract();
+ second->back()->retract();
+ _pm.update();
+ return;
+ }
+
+ if (_drag_initiated && !(event->state & GDK_SHIFT_MASK)) {
+ SnapManager &m = _desktop->namedview->snap_manager;
+ SPItem *path = static_cast<SPItem *>(_pm._path);
+ m.setup(_desktop, true, path); // We will not try to snap to "path" itself
+ Inkscape::SnapCandidatePoint scp(new_pos, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), false);
+ new_pos = sp.getPoint();
+ m.unSetup();
+ }
+
+ // Magic Bezier Drag Equations follow!
+ // "weight" describes how the influence of the drag should be distributed
+ // among the handles; 0 = front handle only, 1 = back handle only.
+ double weight, t = _t;
+ if (t <= 1.0 / 6.0) weight = 0;
+ else if (t <= 0.5) weight = (pow((6 * t - 1) / 2.0, 3)) / 2;
+ else if (t <= 5.0 / 6.0) weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5;
+ else weight = 1;
+
+ Geom::Point delta = new_pos - position();
+ Geom::Point offset0 = ((1-weight)/(3*t*(1-t)*(1-t))) * delta;
+ Geom::Point offset1 = (weight/(3*t*t*(1-t))) * delta;
+
+ //modified so that, if the trace is bspline, it only acts if the SHIFT key is pressed
+ if(!_pm._isBSpline()){
+ first->front()->move(first->front()->position() + offset0);
+ second->back()->move(second->back()->position() + offset1);
+ }else if(weight>=0.8){
+ if(held_shift(*event)){
+ second->back()->move(new_pos);
+ } else {
+ second->move(second->position() + delta);
+ }
+ }else if(weight<=0.2){
+ if(held_shift(*event)){
+ first->back()->move(new_pos);
+ } else {
+ first->move(first->position() + delta);
+ }
+ }else{
+ first->move(first->position() + delta);
+ second->move(second->position() + delta);
+ }
+ _pm.update();
+}
+
+void CurveDragPoint::ungrabbed(GdkEventButton *)
+{
+ _pm._updateDragPoint(_desktop->d2w(position()));
+ _pm._commit(_("Drag curve"));
+ _pm._selection.restoreTransformHandles();
+}
+
+bool CurveDragPoint::clicked(GdkEventButton *event)
+{
+ // This check is probably redundant
+ if (!first || event->button != 1) return false;
+ // the next iterator can be invalid if we click very near the end of path
+ NodeList::iterator second = first.next();
+ if (!second) return false;
+
+ // insert nodes on Ctrl+Alt+click
+ if (held_control(*event) && held_alt(*event)) {
+ _insertNode(false);
+ return true;
+ }
+
+ if (held_shift(*event)) {
+ // if both nodes of the segment are selected, deselect;
+ // otherwise add to selection
+ if (first->selected() && second->selected()) {
+ _pm._selection.erase(first.ptr());
+ _pm._selection.erase(second.ptr());
+ } else {
+ _pm._selection.insert(first.ptr());
+ _pm._selection.insert(second.ptr());
+ }
+ } else {
+ // without Shift, take selection
+ _pm._selection.clear();
+ _pm._selection.insert(first.ptr());
+ _pm._selection.insert(second.ptr());
+ }
+ return true;
+}
+
+bool CurveDragPoint::doubleclicked(GdkEventButton *event)
+{
+ if (event->button != 1 || !first || !first.next()) return false;
+ _insertNode(true);
+ return true;
+}
+
+void CurveDragPoint::_insertNode(bool take_selection)
+{
+ // The purpose of this call is to make way for the just created node.
+ // Otherwise clicks on the new node would only work after the user moves the mouse a bit.
+ // PathManipulator will restore visibility when necessary.
+ setVisible(false);
+
+ _pm.insertNode(first, _t, take_selection);
+}
+
+Glib::ustring CurveDragPoint::_getTip(unsigned state) const
+{
+ if (_pm.empty()) return "";
+ if (!first || !first.next()) return "";
+ bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate();
+ if(state_held_shift(state) && _pm._isBSpline()){
+ return C_("Path segment tip",
+ "<b>Shift</b>: drag to open or move BSpline handles");
+ }
+ if (state_held_shift(state)) {
+ return C_("Path segment tip",
+ "<b>Shift</b>: click to toggle segment selection");
+ }
+ if (state_held_control(state) && state_held_alt(state)) {
+ return C_("Path segment tip",
+ "<b>Ctrl+Alt</b>: click to insert a node");
+ }
+ if(_pm._isBSpline()){
+ return C_("Path segment tip",
+ "<b>BSpline segment</b>: drag to shape the segment, doubleclick to insert node, "
+ "click to select (more: Shift, Ctrl+Alt)");
+ }
+ if (linear) {
+ return C_("Path segment tip",
+ "<b>Linear segment</b>: drag to convert to a Bezier segment, "
+ "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)");
+ } else {
+ return C_("Path segment tip",
+ "<b>Bezier segment</b>: drag to shape the segment, doubleclick to insert node, "
+ "click to select (more: Shift, Ctrl+Alt)");
+ }
+}
+
+} // 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/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h
new file mode 100644
index 0000000..bfe0ad7
--- /dev/null
+++ b/src/ui/tool/curve-drag-point.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_CURVE_DRAG_POINT_H
+#define SEEN_UI_TOOL_CURVE_DRAG_POINT_H
+
+#include "ui/tool/control-point.h"
+#include "ui/tool/node.h"
+
+class SPDesktop;
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+struct PathSharedData;
+
+// This point should be invisible to the user - use the invisible_cset from control-point.h
+// TODO make some methods from path-manipulator.cpp public so that this point doesn't have
+// to be declared as a friend
+/**
+ * An invisible point used to drag curves. This point is used by PathManipulator to allow editing
+ * of path segments by dragging them. It is defined in a separate file so that the node tool
+ * can check if the mouseovered control point is a curve drag point and update the cursor
+ * accordingly, without the need to drag in the full PathManipulator header.
+ */
+class CurveDragPoint : public ControlPoint {
+public:
+
+ CurveDragPoint(PathManipulator &pm);
+ void setSize(double sz) { _setSize(sz); }
+ void setTimeValue(double t) { _t = t; }
+ double getTimeValue() { return _t; }
+ void setIterator(NodeList::iterator i) { first = i; }
+ NodeList::iterator getIterator() { return first; }
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+
+protected:
+
+ Glib::ustring _getTip(unsigned state) const override;
+ void dragged(Geom::Point &, GdkEventMotion *) override;
+ bool grabbed(GdkEventMotion *) override;
+ void ungrabbed(GdkEventButton *) override;
+ bool clicked(GdkEventButton *) override;
+ bool doubleclicked(GdkEventButton *) override;
+
+private:
+ double _t;
+ PathManipulator &_pm;
+ NodeList::iterator first;
+
+ static bool _drags_stroke;
+ static bool _segment_was_degenerate;
+ static Geom::Point _stroke_drag_origin;
+ void _insertNode(bool take_selection);
+};
+
+} // 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/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp
new file mode 100644
index 0000000..33a196d
--- /dev/null
+++ b/src/ui/tool/event-utils.cpp
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Collection of shorthands to deal with GDK events.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm/display.h>
+#include "display/sp-canvas.h"
+#include "ui/tool/event-utils.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+guint shortcut_key(GdkEventKey const &event)
+{
+ guint shortcut_key = 0;
+ gdk_keymap_translate_keyboard_state(
+ Gdk::Display::get_default()->get_keymap(),
+ event.hardware_keycode,
+ (GdkModifierType) event.state,
+ 0 /*event->key.group*/,
+ &shortcut_key, nullptr, nullptr, nullptr);
+ return shortcut_key;
+}
+
+unsigned combine_key_events(guint keyval, gint mask)
+{
+ GdkEvent *event_next;
+ gint i = 0;
+
+ event_next = gdk_event_get();
+ // while the next event is also a key notify with the same keyval and mask,
+ while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type == GDK_KEY_RELEASE)
+ && event_next->key.keyval == keyval
+ && (!mask || event_next->key.state & mask)) {
+ if (event_next->type == GDK_KEY_PRESS)
+ i ++;
+ // kill it
+ gdk_event_free(event_next);
+ // get next
+ event_next = gdk_event_get();
+ }
+ // otherwise, put it back onto the queue
+ if (event_next) gdk_event_put(event_next);
+
+ return i;
+}
+
+unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask)
+{
+ if (canvas == nullptr) {
+ return false;
+ }
+ GdkEvent *event_next;
+ gint i = 0;
+ event.x -= canvas->_x0;
+ event.y -= canvas->_y0;
+
+ event_next = gdk_event_get();
+ // while the next event is also a motion notify
+ while (event_next && (event_next->type == GDK_MOTION_NOTIFY)
+ && (!mask || event_next->motion.state & mask))
+ {
+ if (event_next->motion.device == event.device) {
+ GdkEventMotion &next = event_next->motion;
+ event.send_event = next.send_event;
+ event.time = next.time;
+ event.x = next.x;
+ event.y = next.y;
+ event.state = next.state;
+ event.is_hint = next.is_hint;
+ event.x_root = next.x_root;
+ event.y_root = next.y_root;
+ if (event.axes && next.axes) {
+ memcpy(event.axes, next.axes, gdk_device_get_n_axes(event.device));
+ }
+ }
+
+ // kill it
+ gdk_event_free(event_next);
+ event_next = gdk_event_get();
+ i++;
+ }
+ // otherwise, put it back onto the queue
+ if (event_next) {
+ gdk_event_put(event_next);
+ }
+ event.x += canvas->_x0;
+ event.y += canvas->_y0;
+
+ return i;
+}
+
+/** Returns the modifier state valid after this event. Use this when you process events
+ * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */
+unsigned state_after_event(GdkEvent *event)
+{
+ unsigned state = 0;
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ state = event->key.state;
+ switch(shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ state |= GDK_SHIFT_MASK;
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ state |= GDK_CONTROL_MASK;
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ state |= GDK_MOD1_MASK;
+ break;
+ default: break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ state = event->key.state;
+ switch(shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ state &= ~GDK_SHIFT_MASK;
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ state &= ~GDK_CONTROL_MASK;
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ state &= ~GDK_MOD1_MASK;
+ break;
+ default: break;
+ }
+ break;
+ default: break;
+ }
+ return state;
+}
+
+} // 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/tool/event-utils.h b/src/ui/tool/event-utils.h
new file mode 100644
index 0000000..3fd8f16
--- /dev/null
+++ b/src/ui/tool/event-utils.h
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Collection of shorthands to deal with GDK events.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_EVENT_UTILS_H
+#define SEEN_UI_TOOL_EVENT_UTILS_H
+
+#include <gdk/gdk.h>
+#include <2geom/point.h>
+
+struct SPCanvas;
+
+namespace Inkscape {
+namespace UI {
+
+inline bool state_held_shift(unsigned state) {
+ return state & GDK_SHIFT_MASK;
+}
+inline bool state_held_control(unsigned state) {
+ return state & GDK_CONTROL_MASK;
+}
+inline bool state_held_alt(unsigned state) {
+ return state & GDK_MOD1_MASK;
+}
+inline bool state_held_only_shift(unsigned state) {
+ return (state & GDK_SHIFT_MASK) && !(state & (GDK_CONTROL_MASK | GDK_MOD1_MASK));
+}
+inline bool state_held_only_control(unsigned state) {
+ return (state & GDK_CONTROL_MASK) && !(state & (GDK_SHIFT_MASK | GDK_MOD1_MASK));
+}
+inline bool state_held_only_alt(unsigned state) {
+ return (state & GDK_MOD1_MASK) && !(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK));
+}
+inline bool state_held_any_modifiers(unsigned state) {
+ return state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK);
+}
+inline bool state_held_no_modifiers(unsigned state) {
+ return !state_held_any_modifiers(state);
+}
+template <unsigned button>
+inline bool state_held_button(unsigned state) {
+ return (button == 0 || button > 5) ? false : state & (GDK_BUTTON1_MASK << (button-1));
+}
+
+
+/** Checks whether Shift was held when the event was generated. */
+template <typename E>
+inline bool held_shift(E const &event) {
+ return state_held_shift(event.state);
+}
+
+/** Checks whether Control was held when the event was generated. */
+template <typename E>
+inline bool held_control(E const &event) {
+ return state_held_control(event.state);
+}
+
+/** Checks whether Alt was held when the event was generated. */
+template <typename E>
+inline bool held_alt(E const &event) {
+ return state_held_alt(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Ctrl was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_control(E const &event) {
+ return state_held_only_control(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Shift was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_shift(E const &event) {
+ return state_held_only_shift(event.state);
+}
+
+/** True if from the set of Ctrl, Shift and Alt only Alt was held when the event
+ * was generated. */
+template <typename E>
+inline bool held_only_alt(E const &event) {
+ return state_held_only_alt(event.state);
+}
+
+template <typename E>
+inline bool held_no_modifiers(E const &event) {
+ return state_held_no_modifiers(event.state);
+}
+
+template <typename E>
+inline bool held_any_modifiers(E const &event) {
+ return state_held_any_modifiers(event.state);
+}
+
+template <typename E>
+inline Geom::Point event_point(E const &event) {
+ return Geom::Point(event.x, event.y);
+}
+
+/** Use like this:
+ * @code if (held_button<2>(event->motion)) { ... @endcode */
+template <unsigned button, typename E>
+inline bool held_button(E const &event) {
+ return state_held_button<button>(event.state);
+}
+
+guint shortcut_key(GdkEventKey const &event);
+unsigned combine_key_events(guint keyval, gint mask);
+unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask);
+unsigned state_after_event(GdkEvent *event);
+
+} // 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/tool/manipulator.cpp b/src/ui/tool/manipulator.cpp
new file mode 100644
index 0000000..a68de5e
--- /dev/null
+++ b/src/ui/tool/manipulator.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Manipulator base class and manipulator group - implementation
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+//#include "ui/tool/node.h"
+//#include "ui/tool/manipulator.h"
+
+namespace Inkscape {
+namespace UI {
+
+/*
+void Manipulator::_grabEvents()
+{
+ if (_group) _group->_grabEvents(boost::shared_ptr<Manipulator>(this));
+}
+void Manipulator::_ungrabEvents()
+{
+ if (_group) _group->_ungrabEvents(boost::shared_ptr<Manipulator>(this));
+}
+
+ManipulatorGroup::ManipulatorGroup(SPDesktop *d) :
+ _desktop(d)
+{
+}
+ManipulatorGroup::~ManipulatorGroup()
+{
+}
+
+void ManipulatorGroup::_grabEvents(boost::shared_ptr<Manipulator> m)
+{
+ if (!_grab) _grab = m;
+}
+void ManipulatorGroup::_ungrabEvents(boost::shared_ptr<Manipulator> m)
+{
+ if (_grab == m) _grab.reset();
+}
+
+void ManipulatorGroup::add(boost::shared_ptr<Manipulator> m)
+{
+ m->_group = this;
+ push_back(m);
+}
+void ManipulatorGroup::remove(boost::shared_ptr<Manipulator> m)
+{
+ for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) {
+ if ((*i) == m) {
+ erase(i);
+ break;
+ }
+ }
+ m->_group = 0;
+}
+
+void ManipulatorGroup::clear()
+{
+ std::list<boost::shared_ptr<Manipulator> >::clear();
+}
+
+bool ManipulatorGroup::event(GdkEvent *event)
+{
+ if (_grab) {
+ return _grab->event(event);
+ }
+
+ for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) {
+ if ((*i)->event(event) || _grab) return true;
+ }
+ return false;
+}*/
+
+} // 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/tool/manipulator.h b/src/ui/tool/manipulator.h
new file mode 100644
index 0000000..308ad1c
--- /dev/null
+++ b/src/ui/tool/manipulator.h
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Manipulator - edits something on-canvas
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MANIPULATOR_H
+#define SEEN_UI_TOOL_MANIPULATOR_H
+
+#include <set>
+#include <map>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <glib.h>
+#include <gdk/gdk.h>
+#include "ui/tools/tool-base.h"
+
+class SPDesktop;
+namespace Inkscape {
+namespace UI {
+
+class ManipulatorGroup;
+class ControlPointSelection;
+
+/**
+ * @brief Tool component that processes events and does something in response to them.
+ * Note: this class is probably redundant.
+ */
+class Manipulator {
+friend class ManipulatorGroup;
+public:
+ Manipulator(SPDesktop *d)
+ : _desktop(d)
+ {}
+ virtual ~Manipulator() = default;
+
+ /// Handle input event. Returns true if handled.
+ virtual bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *)=0;
+ SPDesktop *const _desktop;
+};
+
+/**
+ * @brief Tool component that edits something on the canvas using selectable control points.
+ * Note: this class is probably redundant.
+ */
+class PointManipulator : public Manipulator, public sigc::trackable {
+public:
+ PointManipulator(SPDesktop *d, ControlPointSelection &sel)
+ : Manipulator(d)
+ , _selection(sel)
+ {}
+
+ /// Type of extremum points to add in PathManipulator::insertNodeAtExtremum
+ enum ExtremumType {
+ EXTR_MIN_X = 0,
+ EXTR_MAX_X,
+ EXTR_MIN_Y,
+ EXTR_MAX_Y
+ };
+protected:
+ ControlPointSelection &_selection;
+};
+
+/** Manipulator that aggregates several manipulators of the same type.
+ * The order of invoking events on the member manipulators is undefined.
+ * To make this class more useful, derive from it and add actions that can be performed
+ * on all manipulators in the set.
+ *
+ * This is not used at the moment and is probably useless. */
+template <typename T>
+class MultiManipulator : public PointManipulator {
+public:
+ //typedef typename T::ItemType ItemType;
+ typedef typename std::pair<void*, std::shared_ptr<T> > MapPair;
+ typedef typename std::map<void*, std::shared_ptr<T> > MapType;
+
+ MultiManipulator(SPDesktop *d, ControlPointSelection &sel)
+ : PointManipulator(d, sel)
+ {}
+ void addItem(void *item) {
+ std::shared_ptr<T> m(_createManipulator(item));
+ _mmap.insert(MapPair(item, m));
+ }
+ void removeItem(void *item) {
+ _mmap.erase(item);
+ }
+ void clear() {
+ _mmap.clear();
+ }
+ bool contains(void *item) {
+ return _mmap.find(item) != _mmap.end();
+ }
+ bool empty() {
+ return _mmap.empty();
+ }
+
+ void setItems(std::vector<gpointer> list) { // this function is not called anywhere ... delete ?
+ std::set<void*> to_remove;
+ for (typename MapType::iterator mi = _mmap.begin(); mi != _mmap.end(); ++mi) {
+ to_remove.insert(mi->first);
+ }
+ for (auto i:list) {
+ if (_isItemType(i)) {
+ // erase returns the number of items removed
+ // if nothing was removed, it means this item did not have a manipulator - add it
+ if (!to_remove.erase(i)) addItem(i);
+ }
+ }
+ for (auto ri : to_remove) {
+ removeItem(ri);
+ }
+ }
+
+ /** Invoke a method on all managed manipulators.
+ * Example:
+ * @code m.invokeForAll(&SomeManipulator::someMethod); @endcode
+ */
+ template <typename R>
+ void invokeForAll(R (T::*method)()) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)();
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (T::*method)(A), A a) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (T::*method)(A const &), A const &a) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A, typename B>
+ void invokeForAll(R (T::*method)(A,B), A a, B b) {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ ((i->second.get())->*method)(a, b);
+ }
+ }
+
+ bool event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override {
+ for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ if ((*i).second->event(event_context, event)) return true;
+ }
+ return false;
+ }
+protected:
+ virtual T *_createManipulator(void *item) = 0;
+ virtual bool _isItemType(void *item) = 0;
+ MapType _mmap;
+};
+
+} // 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/tool/modifier-tracker.cpp b/src/ui/tool/modifier-tracker.cpp
new file mode 100644
index 0000000..70c85a6
--- /dev/null
+++ b/src/ui/tool/modifier-tracker.cpp
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Fine-grained modifier tracker for event handling.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include "ui/tool/event-utils.h"
+#include "ui/tool/modifier-tracker.h"
+
+namespace Inkscape {
+namespace UI {
+
+ModifierTracker::ModifierTracker()
+ : _left_shift(false)
+ , _right_shift(false)
+ , _left_ctrl(false)
+ , _right_ctrl(false)
+ , _left_alt(false)
+ , _right_alt(false)
+{}
+
+bool ModifierTracker::event(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ _left_shift = true;
+ break;
+ case GDK_KEY_Shift_R:
+ _right_shift = true;
+ break;
+ case GDK_KEY_Control_L:
+ _left_ctrl = true;
+ break;
+ case GDK_KEY_Control_R:
+ _right_ctrl = true;
+ break;
+ case GDK_KEY_Alt_L:
+ _left_alt = true;
+ break;
+ case GDK_KEY_Alt_R:
+ _right_alt = true;
+ break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ switch (shortcut_key(event->key)) {
+ case GDK_KEY_Shift_L:
+ _left_shift = false;
+ break;
+ case GDK_KEY_Shift_R:
+ _right_shift = false;
+ break;
+ case GDK_KEY_Control_L:
+ _left_ctrl = false;
+ break;
+ case GDK_KEY_Control_R:
+ _right_ctrl = false;
+ break;
+ case GDK_KEY_Alt_L:
+ _left_alt = false;
+ break;
+ case GDK_KEY_Alt_R:
+ _right_alt = false;
+ break;
+ }
+ break;
+ default: break;
+ }
+
+ return false;
+}
+
+} // 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/tool/modifier-tracker.h b/src/ui/tool/modifier-tracker.h
new file mode 100644
index 0000000..c5762e5
--- /dev/null
+++ b/src/ui/tool/modifier-tracker.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Fine-grained modifier tracker for event handling.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MODIFIER_TRACKER_H
+#define SEEN_UI_TOOL_MODIFIER_TRACKER_H
+
+#include <gdk/gdk.h>
+
+namespace Inkscape {
+namespace UI {
+
+class ModifierTracker {
+public:
+ ModifierTracker();
+ bool event(GdkEvent *);
+
+ bool leftShift() const { return _left_shift; }
+ bool rightShift() const { return _right_shift; }
+ bool leftControl() const { return _left_ctrl; }
+ bool rightControl() const { return _right_ctrl; }
+ bool leftAlt() const { return _left_alt; }
+ bool rightAlt() const { return _right_alt; }
+
+private:
+ bool _left_shift;
+ bool _right_shift;
+ bool _left_ctrl;
+ bool _right_ctrl;
+ bool _left_alt;
+ bool _right_alt;
+};
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_TOOL_MODIFIER_TRACKER_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/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp
new file mode 100644
index 0000000..bd84cc1
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.cpp
@@ -0,0 +1,888 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Multi path manipulator - implementation.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <unordered_set>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "message-stack.h"
+#include "node.h"
+#include "verbs.h"
+
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-path.h"
+
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+
+struct hash_nodelist_iterator
+ : public std::unary_function<NodeList::iterator, std::size_t>
+{
+ std::size_t operator()(NodeList::iterator i) const {
+ return std::hash<NodeList::iterator::pointer>()(&*i);
+ }
+};
+
+typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
+typedef std::vector<IterPair> IterPairList;
+typedef std::unordered_set<NodeList::iterator, hash_nodelist_iterator> IterSet;
+typedef std::multimap<double, IterPair> DistanceMap;
+typedef std::pair<double, IterPair> DistanceMapItem;
+
+/** Find pairs of selected endnodes suitable for joining. */
+void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
+{
+ IterSet join_iters;
+
+ // find all endnodes in selection
+ for (auto i : sel) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (!node) continue;
+ NodeList::iterator iter = NodeList::get_iterator(node);
+ if (!iter.next() || !iter.prev()) join_iters.insert(iter);
+ }
+
+ if (join_iters.size() < 2) return;
+
+ // Below we find the closest pairs. The algorithm is O(N^3).
+ // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
+ // with their distances in a multimap (not worth it IMO).
+ while (join_iters.size() >= 2) {
+ double closest = DBL_MAX;
+ IterPair closest_pair;
+ for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
+ for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
+ double dist = Geom::distance(**i, **j);
+ if (dist < closest) {
+ closest = dist;
+ closest_pair = std::make_pair(*i, *j);
+ }
+ }
+ }
+ pairs.push_back(closest_pair);
+ join_iters.erase(closest_pair.first);
+ join_iters.erase(closest_pair.second);
+ }
+}
+
+/** After this function, first should be at the end of path and second at the beginnning.
+ * @returns True if the nodes are in the same subpath */
+bool prepare_join(IterPair &join_iters)
+{
+ if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
+ if (join_iters.first.next()) // if first is begin, swap the iterators
+ std::swap(join_iters.first, join_iters.second);
+ return true;
+ }
+
+ NodeList &sp_first = NodeList::get(join_iters.first);
+ NodeList &sp_second = NodeList::get(join_iters.second);
+ if (join_iters.first.next()) { // first is begin
+ if (join_iters.second.next()) { // second is begin
+ sp_first.reverse();
+ } else { // second is end
+ std::swap(join_iters.first, join_iters.second);
+ }
+ } else { // first is end
+ if (join_iters.second.next()) { // second is begin
+ // do nothing
+ } else { // second is end
+ sp_second.reverse();
+ }
+ }
+ return false;
+}
+} // anonymous namespace
+
+
+MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
+ : PointManipulator(data.node_data.desktop, *data.node_data.selection)
+ , _path_data(data)
+ , _changed(chg)
+{
+ _selection.signal_commit.connect(
+ sigc::mem_fun(*this, &MultiPathManipulator::_commit));
+ _selection.signal_selection_changed.connect(
+ sigc::hide( sigc::hide(
+ signal_coords_changed.make_slot())));
+}
+
+MultiPathManipulator::~MultiPathManipulator()
+{
+ _mmap.clear();
+}
+
+/** Remove empty manipulators. */
+void MultiPathManipulator::cleanup()
+{
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
+ if (i->second->empty()) i = _mmap.erase(i);
+ else ++i;
+ }
+}
+
+/**
+ * Change the set of items to edit.
+ *
+ * This method attempts to preserve as much of the state as possible.
+ */
+void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
+{
+ std::set<ShapeRecord> shapes(s);
+
+ // iterate over currently edited items, modifying / removing them as necessary
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
+ std::set<ShapeRecord>::iterator si = shapes.find(i->first);
+ if (si == shapes.end()) {
+ // This item is no longer supposed to be edited - remove its manipulator
+ i = _mmap.erase(i);
+ } else {
+ ShapeRecord const &sr = i->first;
+ ShapeRecord const &sr_new = *si;
+ // if the shape record differs, replace the key only and modify other values
+ if (sr.edit_transform != sr_new.edit_transform ||
+ sr.role != sr_new.role)
+ {
+ std::shared_ptr<PathManipulator> hold(i->second);
+ if (sr.edit_transform != sr_new.edit_transform)
+ hold->setControlsTransform(sr_new.edit_transform);
+ if (sr.role != sr_new.role) {
+ //hold->setOutlineColor(_getOutlineColor(sr_new.role));
+ }
+ i = _mmap.erase(i);
+ _mmap.insert(std::make_pair(sr_new, hold));
+ } else {
+ ++i;
+ }
+ shapes.erase(si); // remove the processed record
+ }
+ }
+
+ // add newly selected items
+ for (const auto & r : shapes) {
+ LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(r.object);
+ if (!SP_IS_PATH(r.object) && !lpobj) continue;
+ std::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.object,
+ r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key));
+ newpm->showHandles(_show_handles);
+ // always show outlines for clips and masks
+ newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
+ newpm->showPathDirection(_show_path_direction);
+ newpm->setLiveOutline(_live_outline);
+ newpm->setLiveObjects(_live_objects);
+ _mmap.insert(std::make_pair(r, newpm));
+ }
+}
+
+void MultiPathManipulator::selectSubpaths()
+{
+ if (_selection.empty()) {
+ _selection.selectAll();
+ } else {
+ invokeForAll(&PathManipulator::selectSubpaths);
+ }
+}
+
+void MultiPathManipulator::shiftSelection(int dir)
+{
+ if (empty()) return;
+
+ // 1. find last selected node
+ // 2. select the next node; if the last node or nothing is selected,
+ // select first node
+ MapType::iterator last_i;
+ SubpathList::iterator last_j;
+ NodeList::iterator last_k;
+ bool anything_found = false;
+ bool anynode_found = false;
+
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ SubpathList &sp = i->second->subpathList();
+ for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) {
+ anynode_found = true;
+ for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) {
+ if (k->selected()) {
+ last_i = i;
+ last_j = j;
+ last_k = k;
+ anything_found = true;
+ // when tabbing backwards, we want the first node
+ if (dir == -1) goto exit_loop;
+ }
+ }
+ }
+ }
+ exit_loop:
+
+ // NOTE: we should not assume the _selection contains only nodes
+ // in future it might also contain handles and other types of control points
+ // this is why we use a flag instead in the loop above, instead of calling
+ // selection.empty()
+ if (!anything_found) {
+ // select first / last node
+ // this should never fail because there must be at least 1 non-empty manipulator
+ if (anynode_found) {
+ if (dir == 1) {
+ _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr());
+ } else {
+ _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr());
+ }
+ }
+ return;
+ }
+
+ // three levels deep - w00t!
+ if (dir == 1) {
+ if (++last_k == (*last_j)->end()) {
+ // here, last_k points to the node to be selected
+ ++last_j;
+ if (last_j == last_i->second->subpathList().end()) {
+ ++last_i;
+ if (last_i == _mmap.end()) {
+ last_i = _mmap.begin();
+ }
+ last_j = last_i->second->subpathList().begin();
+ }
+ last_k = (*last_j)->begin();
+ }
+ } else {
+ if (!last_k || last_k == (*last_j)->begin()) {
+ if (last_j == last_i->second->subpathList().begin()) {
+ if (last_i == _mmap.begin()) {
+ last_i = _mmap.end();
+ }
+ --last_i;
+ last_j = last_i->second->subpathList().end();
+ }
+ --last_j;
+ last_k = (*last_j)->end();
+ }
+ --last_k;
+ }
+ _selection.clear();
+ _selection.insert(last_k.ptr());
+}
+
+void MultiPathManipulator::invertSelectionInSubpaths()
+{
+ invokeForAll(&PathManipulator::invertSelectionInSubpaths);
+}
+
+void MultiPathManipulator::setNodeType(NodeType type)
+{
+ if (_selection.empty()) return;
+
+ // When all selected nodes are already cusp, retract their handles
+ bool retract_handles = (type == NODE_CUSP);
+
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ retract_handles &= (node->type() == NODE_CUSP);
+ node->setType(type);
+ }
+ }
+
+ if (retract_handles) {
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ node->front()->retract();
+ node->back()->retract();
+ }
+ }
+ }
+
+ _done(retract_handles ? _("Retract handles") : _("Change node type"));
+}
+
+void MultiPathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::setSegmentType, type);
+ if (type == SEGMENT_STRAIGHT) {
+ _done(_("Straighten segments"));
+ } else {
+ _done(_("Make segments curves"));
+ }
+}
+
+void MultiPathManipulator::insertNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodes);
+ _done(_("Add nodes"));
+}
+void MultiPathManipulator::insertNodesAtExtrema(ExtremumType extremum)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodeAtExtremum, extremum);
+ _done(_("Add extremum nodes"));
+}
+
+void MultiPathManipulator::insertNode(Geom::Point pt)
+{
+ // When double clicking to insert nodes, we might not have a selection of nodes (and we don't need one)
+ // so don't check for "_selection.empty()" here, contrary to the other methods above and below this one
+ invokeForAll(&PathManipulator::insertNode, pt);
+ _done(_("Add nodes"));
+}
+
+void MultiPathManipulator::duplicateNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::duplicateNodes);
+ _done(_("Duplicate nodes"));
+}
+
+void MultiPathManipulator::joinNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::hideDragPoint);
+ // Node join has two parts. In the first one we join two subpaths by fusing endpoints
+ // into one. In the second we fuse nodes in each subpath.
+ IterPairList joins;
+ NodeList::iterator preserve_pos;
+ Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
+ if (mouseover_node) {
+ preserve_pos = NodeList::get_iterator(mouseover_node);
+ }
+ find_join_iterators(_selection, joins);
+
+ for (auto & join : joins) {
+ bool same_path = prepare_join(join);
+ NodeList &sp_first = NodeList::get(join.first);
+ NodeList &sp_second = NodeList::get(join.second);
+ join.first->setType(NODE_CUSP, false);
+
+ Geom::Point joined_pos, pos_handle_front, pos_handle_back;
+ pos_handle_front = *join.second->front();
+ pos_handle_back = *join.first->back();
+
+ // When we encounter the mouseover node, we unset the iterator - it will be invalidated
+ if (join.first == preserve_pos) {
+ joined_pos = *join.first;
+ preserve_pos = NodeList::iterator();
+ } else if (join.second == preserve_pos) {
+ joined_pos = *join.second;
+ preserve_pos = NodeList::iterator();
+ } else {
+ joined_pos = Geom::middle_point(*join.first, *join.second);
+ }
+
+ // if the handles aren't degenerate, don't move them
+ join.first->move(joined_pos);
+ Node *joined_node = join.first.ptr();
+ if (!join.second->front()->isDegenerate()) {
+ joined_node->front()->setPosition(pos_handle_front);
+ }
+ if (!join.first->back()->isDegenerate()) {
+ joined_node->back()->setPosition(pos_handle_back);
+ }
+ sp_second.erase(join.second);
+
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ _selection.insert(join.first.ptr());
+ }
+
+ if (joins.empty()) {
+ // Second part replaces contiguous selections of nodes with single nodes
+ invokeForAll(&PathManipulator::weldNodes, preserve_pos);
+ }
+
+ _doneWithCleanup(_("Join nodes"), true);
+}
+
+void MultiPathManipulator::breakNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::breakNodes);
+ _done(_("Break nodes"), true);
+}
+
+void MultiPathManipulator::deleteNodes(bool keep_shape)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteNodes, keep_shape);
+ _doneWithCleanup(_("Delete nodes"), true);
+}
+
+/** Join selected endpoints to create segments. */
+void MultiPathManipulator::joinSegments()
+{
+ if (_selection.empty()) return;
+ IterPairList joins;
+ find_join_iterators(_selection, joins);
+
+ for (auto & join : joins) {
+ bool same_path = prepare_join(join);
+ NodeList &sp_first = NodeList::get(join.first);
+ NodeList &sp_second = NodeList::get(join.second);
+ join.first->setType(NODE_CUSP, false);
+ join.second->setType(NODE_CUSP, false);
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ }
+
+ if (joins.empty()) {
+ invokeForAll(&PathManipulator::weldSegments);
+ }
+ _doneWithCleanup("Join segments", true);
+}
+
+void MultiPathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteSegments);
+ _doneWithCleanup("Delete segments", true);
+}
+
+void MultiPathManipulator::alignNodes(Geom::Dim2 d)
+{
+ if (_selection.empty()) return;
+ _selection.align(d);
+ if (d == Geom::X) {
+ _done("Align nodes to a horizontal line");
+ } else {
+ _done("Align nodes to a vertical line");
+ }
+}
+
+void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
+{
+ if (_selection.empty()) return;
+ _selection.distribute(d);
+ if (d == Geom::X) {
+ _done("Distribute nodes horizontally");
+ } else {
+ _done("Distribute nodes vertically");
+ }
+}
+
+void MultiPathManipulator::reverseSubpaths()
+{
+ if (_selection.empty()) {
+ invokeForAll(&PathManipulator::reverseSubpaths, false);
+ _done("Reverse subpaths");
+ } else {
+ invokeForAll(&PathManipulator::reverseSubpaths, true);
+ _done("Reverse selected subpaths");
+ }
+}
+
+void MultiPathManipulator::move(Geom::Point const &delta)
+{
+ if (_selection.empty()) return;
+ _selection.transform(Geom::Translate(delta));
+ _done("Move nodes");
+}
+
+void MultiPathManipulator::showOutline(bool show)
+{
+ for (auto & i : _mmap) {
+ // always show outlines for clipping paths and masks
+ i.second->showOutline(show || i.first.role != SHAPE_ROLE_NORMAL);
+ }
+ _show_outline = show;
+}
+
+void MultiPathManipulator::showHandles(bool show)
+{
+ invokeForAll(&PathManipulator::showHandles, show);
+ _show_handles = show;
+}
+
+void MultiPathManipulator::showPathDirection(bool show)
+{
+ invokeForAll(&PathManipulator::showPathDirection, show);
+ _show_path_direction = show;
+}
+
+/**
+ * Set live outline update status.
+ * When set to true, outline will be updated continuously when dragging
+ * or transforming nodes. Otherwise it will only update when changes are committed
+ * to XML.
+ */
+void MultiPathManipulator::setLiveOutline(bool set)
+{
+ invokeForAll(&PathManipulator::setLiveOutline, set);
+ _live_outline = set;
+}
+
+/**
+ * Set live object update status.
+ * When set to true, objects will be updated continuously when dragging
+ * or transforming nodes. Otherwise they will only update when changes are committed
+ * to XML.
+ */
+void MultiPathManipulator::setLiveObjects(bool set)
+{
+ invokeForAll(&PathManipulator::setLiveObjects, set);
+ _live_objects = set;
+}
+
+void MultiPathManipulator::updateOutlineColors()
+{
+ //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ // i->second->setOutlineColor(_getOutlineColor(i->first.role));
+ //}
+}
+
+void MultiPathManipulator::updateHandles()
+{
+ invokeForAll(&PathManipulator::updateHandles);
+}
+
+bool MultiPathManipulator::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ _tracker.event(event);
+ guint key = 0;
+ if (event->type == GDK_KEY_PRESS) {
+ key = shortcut_key(event->key);
+ }
+
+ // Single handle adjustments go here.
+ if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) {
+ do {
+ Node *n = dynamic_cast<Node *>(*_selection.begin());
+ if (!n) break;
+
+ PathManipulator &pm = n->nodeList().subpathList().pm();
+
+ int which = 0;
+ if (_tracker.rightAlt() || _tracker.rightControl()) {
+ which = 1;
+ }
+ if (_tracker.leftAlt() || _tracker.leftControl()) {
+ if (which != 0) break; // ambiguous
+ which = -1;
+ }
+ if (which == 0) break; // no handle chosen
+ bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt();
+ bool handled = true;
+
+ switch (key) {
+ // single handle functions
+ // rotation
+ case GDK_KEY_bracketleft:
+ case GDK_KEY_braceleft:
+ pm.rotateHandle(n, which, -_desktop->yaxisdir(), one_pixel);
+ break;
+ case GDK_KEY_bracketright:
+ case GDK_KEY_braceright:
+ pm.rotateHandle(n, which, _desktop->yaxisdir(), one_pixel);
+ break;
+ // adjust length
+ case GDK_KEY_period:
+ case GDK_KEY_greater:
+ pm.scaleHandle(n, which, 1, one_pixel);
+ break;
+ case GDK_KEY_comma:
+ case GDK_KEY_less:
+ pm.scaleHandle(n, which, -1, one_pixel);
+ break;
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled) return true;
+ } while(false);
+ }
+
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (key) {
+ case GDK_KEY_Insert:
+ case GDK_KEY_KP_Insert:
+ // Insert - insert nodes in the middle of selected segments
+ insertNodes();
+ return true;
+ case GDK_KEY_i:
+ case GDK_KEY_I:
+ if (held_only_shift(event->key)) {
+ // Shift+I - insert nodes (alternate keybinding for Mac keyboards
+ // that don't have the Insert key)
+ insertNodes();
+ return true;
+ }
+ break;
+ case GDK_KEY_d:
+ case GDK_KEY_D:
+ if (held_only_shift(event->key)) {
+ duplicateNodes();
+ return true;
+ }
+ case GDK_KEY_j:
+ case GDK_KEY_J:
+ if (held_only_shift(event->key)) {
+ // Shift+J - join nodes
+ joinNodes();
+ return true;
+ }
+ if (held_only_alt(event->key)) {
+ // Alt+J - join segments
+ joinSegments();
+ return true;
+ }
+ break;
+ case GDK_KEY_b:
+ case GDK_KEY_B:
+ if (held_only_shift(event->key)) {
+ // Shift+B - break nodes
+ breakNodes();
+ return true;
+ }
+ break;
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ if (held_shift(event->key)) break;
+ if (held_alt(event->key)) {
+ // Alt+Delete - delete segments
+ deleteSegments();
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true);
+ // pass keep_shape = true when:
+ // a) del preserves shape, and control is not pressed
+ // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed
+ // Hence xor
+ guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0);
+
+ //if the trace is bspline ( mode 2)
+ if(mode==2){
+ // is this correct ?
+ if(del_preserves_shape ^ held_control(event->key)){
+ deleteNodes(false);
+ } else {
+ deleteNodes(true);
+ }
+ } else {
+ deleteNodes(del_preserves_shape ^ held_control(event->key));
+ }
+
+ // Delete any selected gradient nodes as well
+ event_context->deleteSelectedDrag(held_control(event->key));
+ }
+ return true;
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ if (held_only_shift(event->key)) {
+ // Shift+C - make nodes cusp
+ setNodeType(NODE_CUSP);
+ return true;
+ }
+ break;
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+ if (held_only_shift(event->key)) {
+ // Shift+S - make nodes smooth
+ setNodeType(NODE_SMOOTH);
+ return true;
+ }
+ break;
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (held_only_shift(event->key)) {
+ // Shift+A - make nodes auto-smooth
+ setNodeType(NODE_AUTO);
+ return true;
+ }
+ break;
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+ if (held_only_shift(event->key)) {
+ // Shift+Y - make nodes symmetric
+ setNodeType(NODE_SYMMETRIC);
+ return true;
+ }
+ break;
+ case GDK_KEY_r:
+ case GDK_KEY_R:
+ if (held_only_shift(event->key)) {
+ // Shift+R - reverse subpaths
+ reverseSubpaths();
+ return true;
+ }
+ break;
+ case GDK_KEY_l:
+ case GDK_KEY_L:
+ if (held_only_shift(event->key)) {
+ // Shift+L - make segments linear
+ setSegmentType(SEGMENT_STRAIGHT);
+ return true;
+ }
+ case GDK_KEY_u:
+ case GDK_KEY_U:
+ if (held_only_shift(event->key)) {
+ // Shift+U - make segments curves
+ setSegmentType(SEGMENT_CUBIC_BEZIER);
+ return true;
+ }
+ default:
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ combine_motion_events(_desktop->canvas, event->motion, 0);
+ for (auto & i : _mmap) {
+ if (i.second->event(event_context, event)) return true;
+ }
+ break;
+ default: break;
+ }
+
+ return false;
+}
+
+/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
+ * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
+void MultiPathManipulator::_commit(CommitEvent cps)
+{
+ gchar const *reason = nullptr;
+ gchar const *key = nullptr;
+ switch(cps) {
+ case COMMIT_MOUSE_MOVE:
+ reason = _("Move nodes");
+ break;
+ case COMMIT_KEYBOARD_MOVE_X:
+ reason = _("Move nodes horizontally");
+ key = "node:move:x";
+ break;
+ case COMMIT_KEYBOARD_MOVE_Y:
+ reason = _("Move nodes vertically");
+ key = "node:move:y";
+ break;
+ case COMMIT_MOUSE_ROTATE:
+ reason = _("Rotate nodes");
+ break;
+ case COMMIT_KEYBOARD_ROTATE:
+ reason = _("Rotate nodes");
+ key = "node:rotate";
+ break;
+ case COMMIT_MOUSE_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ break;
+ case COMMIT_MOUSE_SCALE:
+ reason = _("Scale nodes");
+ break;
+ case COMMIT_KEYBOARD_SCALE_UNIFORM:
+ reason = _("Scale nodes uniformly");
+ key = "node:scale:uniform";
+ break;
+ case COMMIT_KEYBOARD_SCALE_X:
+ reason = _("Scale nodes horizontally");
+ key = "node:scale:x";
+ break;
+ case COMMIT_KEYBOARD_SCALE_Y:
+ reason = _("Scale nodes vertically");
+ key = "node:scale:y";
+ break;
+ case COMMIT_MOUSE_SKEW_X:
+ reason = _("Skew nodes horizontally");
+ key = "node:skew:x";
+ break;
+ case COMMIT_MOUSE_SKEW_Y:
+ reason = _("Skew nodes vertically");
+ key = "node:skew:y";
+ break;
+ case COMMIT_FLIP_X:
+ reason = _("Flip nodes horizontally");
+ break;
+ case COMMIT_FLIP_Y:
+ reason = _("Flip nodes vertically");
+ break;
+ default: return;
+ }
+
+ _selection.signal_update.emit();
+ invokeForAll(&PathManipulator::writeXML);
+ if (key) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), key, SP_VERB_CONTEXT_NODE, reason);
+ } else {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, reason);
+ }
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML and adds undo stack entry. */
+void MultiPathManipulator::_done(gchar const *reason, bool alert_LPE) {
+ invokeForAll(&PathManipulator::update, alert_LPE);
+ invokeForAll(&PathManipulator::writeXML);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, reason);
+ signal_coords_changed.emit();
+}
+
+/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
+void MultiPathManipulator::_doneWithCleanup(gchar const *reason, bool alert_LPE) {
+ _changed.block();
+ _done(reason, alert_LPE);
+ cleanup();
+ _changed.unblock();
+}
+
+/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
+guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ switch(role) {
+ case SHAPE_ROLE_CLIPPING_PATH:
+ return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
+ case SHAPE_ROLE_MASK:
+ return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
+ case SHAPE_ROLE_LPE_PARAM:
+ return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
+ case SHAPE_ROLE_NORMAL:
+ default:
+ return SP_ITEM(object)->highlight_color();
+ }
+}
+
+} // 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/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h
new file mode 100644
index 0000000..7fbb959
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.h
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Multi path manipulator - a tool component that edits multiple paths at once
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H
+#define SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H
+
+#include <cstddef>
+#include <sigc++/connection.h>
+#include "node.h"
+#include "commit-events.h"
+#include "manipulator.h"
+#include "modifier-tracker.h"
+#include "node-types.h"
+#include "shape-record.h"
+
+struct SPCanvasGroup;
+
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+struct PathSharedData;
+
+/**
+ * Manipulator that manages multiple path manipulators active at the same time.
+ */
+class MultiPathManipulator : public PointManipulator {
+public:
+ MultiPathManipulator(PathSharedData &data, sigc::connection &chg);
+ ~MultiPathManipulator() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *event) override;
+
+ bool empty() { return _mmap.empty(); }
+ unsigned size() { return _mmap.size(); }
+ void setItems(std::set<ShapeRecord> const &);
+ void clear() { _mmap.clear(); }
+ void cleanup();
+
+ void selectSubpaths();
+ void shiftSelection(int dir);
+ void invertSelectionInSubpaths();
+
+ void setNodeType(NodeType t);
+ void setSegmentType(SegmentType t);
+
+ void insertNodesAtExtrema(ExtremumType extremum);
+ void insertNodes();
+ void insertNode(Geom::Point pt);
+ void alertLPE();
+ void duplicateNodes();
+ void joinNodes();
+ void breakNodes();
+ void deleteNodes(bool keep_shape = true);
+ void joinSegments();
+ void deleteSegments();
+ void alignNodes(Geom::Dim2 d);
+ void distributeNodes(Geom::Dim2 d);
+ void reverseSubpaths();
+ void move(Geom::Point const &delta);
+
+ void showOutline(bool show);
+ void showHandles(bool show);
+ void showPathDirection(bool show);
+ void setLiveOutline(bool set);
+ void setLiveObjects(bool set);
+ void updateOutlineColors();
+ void updateHandles();
+
+ sigc::signal<void> signal_coords_changed; /// Emitted whenever the coordinates
+ /// shown in the status bar need updating
+private:
+ typedef std::pair<ShapeRecord, std::shared_ptr<PathManipulator> > MapPair;
+ typedef std::map<ShapeRecord, std::shared_ptr<PathManipulator> > MapType;
+
+ template <typename R>
+ void invokeForAll(R (PathManipulator::*method)()) {
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
+ // Sometimes the PathManipulator got freed at loop end, thus
+ // invalidating the iterator so make sure that next_i will
+ // be a valid iterator and then assign i to it.
+ MapType::iterator next_i = i;
+ ++next_i;
+ // i->second is a std::shared_ptr so try to hold on to it so
+ // it won't get freed prematurely by the WriteXML() method or
+ // whatever. See https://bugs.launchpad.net/inkscape/+bug/1617615
+ // Applicable to empty paths.
+ std::shared_ptr<PathManipulator> hold(i->second);
+ ((hold.get())->*method)();
+ i = next_i;
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (PathManipulator::*method)(A), A a) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A>
+ void invokeForAll(R (PathManipulator::*method)(A const &), A const &a) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a);
+ }
+ }
+ template <typename R, typename A, typename B>
+ void invokeForAll(R (PathManipulator::*method)(A,B), A a, B b) {
+ for (auto & i : _mmap) {
+ ((i.second.get())->*method)(a, b);
+ }
+ }
+
+ void _commit(CommitEvent cps);
+ void _done(gchar const *reason, bool alert_LPE = true);
+ void _doneWithCleanup(gchar const *reason, bool alert_LPE = false);
+ guint32 _getOutlineColor(ShapeRole role, SPObject *object);
+
+ MapType _mmap;
+public:
+ PathSharedData const &_path_data;
+private:
+ sigc::connection &_changed;
+ ModifierTracker _tracker;
+ bool _show_handles;
+ bool _show_outline;
+ bool _show_path_direction;
+ bool _live_outline;
+ bool _live_objects;
+
+ friend class PathManipulator;
+};
+
+} // 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/tool/node-types.h b/src/ui/tool/node-types.h
new file mode 100644
index 0000000..ee2d27f
--- /dev/null
+++ b/src/ui/tool/node-types.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Node types and other small enums.
+ * This file exists to reduce the number of includes pulled in by toolbox.cpp.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_NODE_TYPES_H
+#define SEEN_UI_TOOL_NODE_TYPES_H
+
+namespace Inkscape {
+namespace UI {
+
+/** Types of nodes supported in the node tool. */
+enum NodeType {
+ NODE_CUSP, ///< Cusp node - no handle constraints
+ NODE_SMOOTH, ///< Smooth node - handles must be colinear
+ NODE_AUTO, ///< Auto node - handles adjusted automatically based on neighboring nodes
+ NODE_SYMMETRIC, ///< Symmetric node - handles must be colinear and of equal length
+ NODE_LAST_REAL_TYPE, ///< Last real type of node - used for ctrl+click on a node
+ NODE_PICK_BEST = 100 ///< Select type based on handle positions
+};
+
+/** Types of segments supported in the node tool. */
+enum SegmentType {
+ SEGMENT_STRAIGHT, ///< Straight linear segment
+ SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points
+};
+
+} // 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/tool/node.cpp b/src/ui/tool/node.cpp
new file mode 100644
index 0000000..fc09ca9
--- /dev/null
+++ b/src/ui/tool/node.cpp
@@ -0,0 +1,1924 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <atomic>
+#include <iostream>
+#include <stdexcept>
+#include <boost/utility.hpp>
+
+#include <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <2geom/bezier-utils.h>
+
+#include "desktop.h"
+#include "multi-path-manipulator.h"
+#include "snap.h"
+
+#include "display/sp-ctrlline.h"
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-util.h"
+
+#include "ui/control-manager.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools-switch.h"
+
+namespace {
+
+Inkscape::ControlType nodeTypeToCtrlType(Inkscape::UI::NodeType type)
+{
+ Inkscape::ControlType result = Inkscape::CTRL_TYPE_NODE_CUSP;
+ switch(type) {
+ case Inkscape::UI::NODE_SMOOTH:
+ result = Inkscape::CTRL_TYPE_NODE_SMOOTH;
+ break;
+ case Inkscape::UI::NODE_AUTO:
+ result = Inkscape::CTRL_TYPE_NODE_AUTO;
+ break;
+ case Inkscape::UI::NODE_SYMMETRIC:
+ result = Inkscape::CTRL_TYPE_NODE_SYMETRICAL;
+ break;
+ case Inkscape::UI::NODE_CUSP:
+ default:
+ result = Inkscape::CTRL_TYPE_NODE_CUSP;
+ break;
+ }
+ return result;
+}
+
+/**
+ * @brief provides means to estimate float point rounding error due to serialization to svg
+ *
+ * Keeps cached value up to date with preferences option `/options/svgoutput/numericprecision`
+ * to avoid costly direct reads
+ * */
+class SvgOutputPrecisionWatcher : public Inkscape::Preferences::Observer {
+public:
+ /// Returns absolute \a value`s rounding serialization error based on current preferences settings
+ static double error_of(double value) {
+ return value * instance().rel_error;
+ }
+
+ void notify(const Inkscape::Preferences::Entry &new_val) override {
+ int digits = new_val.getIntLimited(6, 1, 16);
+ set_numeric_precision(digits);
+ }
+
+private:
+ SvgOutputPrecisionWatcher() : Observer("/options/svgoutput/numericprecision"), rel_error(1) {
+ Inkscape::Preferences::get()->addObserver(*this);
+ int digits = Inkscape::Preferences::get()->getIntLimited("/options/svgoutput/numericprecision", 6, 1, 16);
+ set_numeric_precision(digits);
+ }
+
+ ~SvgOutputPrecisionWatcher() override {
+ Inkscape::Preferences::get()->removeObserver(*this);
+ }
+ /// Update cached value of relative error with number of significant digits
+ void set_numeric_precision(int digits) {
+ double relative_error = 0.5; // the error is half of last digit
+ while (digits > 0) {
+ relative_error /= 10;
+ digits--;
+ }
+ rel_error = relative_error;
+ }
+
+ static SvgOutputPrecisionWatcher &instance() {
+ static SvgOutputPrecisionWatcher _instance;
+ return _instance;
+ }
+
+ std::atomic<double> rel_error; /// Cached relative error
+};
+
+/// Returns absolute error of \a point as if serialized to svg with current preferences
+double serializing_error_of(const Geom::Point &point) {
+ return SvgOutputPrecisionWatcher::error_of(point.length());
+}
+
+/**
+ * @brief Returns true if three points are collinear within current serializing precision
+ *
+ * The algorithm of collinearity check is explicitly used to calculate the check error.
+ *
+ * This function can be sufficiently reduced or even removed completely if `Geom::are_collinear`
+ * would declare it's check algorithm as part of the public API.
+ *
+ * */
+bool are_collinear_within_serializing_error(const Geom::Point &A, const Geom::Point &B, const Geom::Point &C) {
+ const double tolerance_factor = 10; // to account other factors which increase uncertainty
+ const double tolerance_A = serializing_error_of(A) * tolerance_factor;
+ const double tolerance_B = serializing_error_of(B) * tolerance_factor;
+ const double tolerance_C = serializing_error_of(C) * tolerance_factor;
+ const double CB_length = (B - C).length();
+ const double AB_length = (B - A).length();
+ Geom::Point C_reflect_scaled = B + (B - C) / CB_length * AB_length;
+ double tolerance_C_reflect_scaled = tolerance_B
+ + (tolerance_B + tolerance_C)
+ * (1 + (tolerance_A + tolerance_B) / AB_length)
+ * (1 + (tolerance_C + tolerance_B) / CB_length);
+ return Geom::are_near(C_reflect_scaled, A, tolerance_C_reflect_scaled + tolerance_A);
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+
+const double NO_POWER = 0.0;
+const double DEFAULT_START_POWER = 1.0/3.0;
+
+ControlPoint::ColorSet Node::node_colors = {
+ {0xbfbfbf00, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+ControlPoint::ColorSet Handle::_handle_colors = {
+ {0xffffffff, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff}, // clicked fill, stroke
+ //
+ {0xffffffff, 0x000000ff}, // normal fill, stroke
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke
+ {0xff000000, 0x000000ff} // clicked fill, stroke
+};
+
+std::ostream &operator<<(std::ostream &out, NodeType type)
+{
+ switch(type) {
+ case NODE_CUSP: out << 'c'; break;
+ case NODE_SMOOTH: out << 's'; break;
+ case NODE_AUTO: out << 'a'; break;
+ case NODE_SYMMETRIC: out << 'z'; break;
+ default: out << 'b'; break;
+ }
+ return out;
+}
+
+/** Computes an unit vector of the direction from first to second control point */
+static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) {
+ return Geom::unit_vector(second - first);
+}
+
+Geom::Point Handle::_saved_other_pos(0, 0);
+
+double Handle::_saved_length = 0.0;
+
+bool Handle::_drag_out = false;
+
+Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent) :
+ ControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER,
+ CTRL_TYPE_ADJ_HANDLE,
+ _handle_colors, data.handle_group),
+ _parent(parent),
+ _handle_line(ControlManager::getManager().createControlLine(data.handle_line_group)),
+ _degenerate(true)
+{
+ setVisible(false);
+}
+
+Handle::~Handle()
+{
+ //sp_canvas_item_hide(_handle_line);
+ sp_canvas_item_destroy(_handle_line);
+}
+
+void Handle::setVisible(bool v)
+{
+ ControlPoint::setVisible(v);
+ if (v) {
+ sp_canvas_item_show(_handle_line);
+ } else {
+ sp_canvas_item_hide(_handle_line);
+ }
+}
+
+void Handle::move(Geom::Point const &new_pos)
+{
+ Handle *other = this->other();
+ Node *node_towards = _parent->nodeToward(this); // node in direction of this handle
+ Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction
+ Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : nullptr;
+ Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : nullptr;
+ double bspline_weight = 0.0;
+
+ if (Geom::are_near(new_pos, _parent->position())) {
+ // The handle becomes degenerate.
+ // Adjust node type as necessary.
+ if (other->isDegenerate()) {
+ // If both handles become degenerate, convert to parent cusp node
+ _parent->setType(NODE_CUSP, false);
+ } else {
+ // Only 1 handle becomes degenerate
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ case NODE_SYMMETRIC:
+ _parent->setType(NODE_SMOOTH, false);
+ break;
+ default:
+ // do nothing for other node types
+ break;
+ }
+ }
+ // If the segment between the handle and the node in its direction becomes linear,
+ // and there are smooth nodes at its ends, make their handles collinear with the segment.
+ if (towards && towards_second->isDegenerate()) {
+ if (node_towards->type() == NODE_SMOOTH) {
+ towards->setDirection(*_parent, *node_towards);
+ }
+ if (_parent->type() == NODE_SMOOTH) {
+ other->setDirection(*node_towards, *_parent);
+ }
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+ return;
+ }
+
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ // restrict movement to the line joining the nodes
+ Geom::Point direction = _parent->position() - node_away->position();
+ Geom::Point delta = new_pos - _parent->position();
+ // project the relative position on the direction line
+ Geom::Coord direction_length = Geom::L2sq(direction);
+ Geom::Point new_delta;
+ if (direction_length == 0) {
+ // joining line has zero length - any direction is okay, prevent division by zero
+ new_delta = delta;
+ } else {
+ new_delta = (Geom::dot(delta, direction) / direction_length) * direction;
+ }
+ setRelativePos(new_delta);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+
+ return;
+ }
+
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ _parent->setType(NODE_SMOOTH, false);
+ // fall through - auto nodes degrade into smooth nodes
+ case NODE_SMOOTH: {
+ // for smooth nodes, we need to rotate the opposite handle
+ // so that it's collinear with the dragged one, while conserving length.
+ other->setDirection(new_pos, *_parent);
+ } break;
+ case NODE_SYMMETRIC:
+ // for symmetric nodes, place the other handle on the opposite side
+ other->setRelativePos(-(new_pos - _parent->position()));
+ break;
+ default: break;
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+ Inkscape::UI::Tools::sp_update_helperpath();
+}
+
+void Handle::setPosition(Geom::Point const &p)
+{
+ ControlPoint::setPosition(p);
+ _handle_line->setCoords(_parent->position(), position());
+
+ // update degeneration info and visibility
+ if (Geom::are_near(position(), _parent->position()))
+ _degenerate = true;
+ else _degenerate = false;
+
+ if (_parent->_handles_shown && _parent->visible() && !_degenerate) {
+ setVisible(true);
+ } else {
+ setVisible(false);
+ }
+}
+
+void Handle::setLength(double len)
+{
+ if (isDegenerate()) return;
+ Geom::Point dir = Geom::unit_vector(relativePos());
+ setRelativePos(dir * len);
+}
+
+void Handle::retract()
+{
+ move(_parent->position());
+}
+
+void Handle::setDirection(Geom::Point const &from, Geom::Point const &to)
+{
+ setDirection(to - from);
+}
+
+void Handle::setDirection(Geom::Point const &dir)
+{
+ Geom::Point unitdir = Geom::unit_vector(dir);
+ setRelativePos(unitdir * length());
+}
+
+/**
+ * See also: Node::node_type_to_localized_string(NodeType type)
+ */
+char const *Handle::handle_type_to_localized_string(NodeType type)
+{
+ switch(type) {
+ case NODE_CUSP:
+ return _("Corner node handle");
+ case NODE_SMOOTH:
+ return _("Smooth node handle");
+ case NODE_SYMMETRIC:
+ return _("Symmetric node handle");
+ case NODE_AUTO:
+ return _("Auto-smooth node handle");
+ default:
+ return "";
+ }
+}
+
+bool Handle::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ switch (event->type)
+ {
+ case GDK_KEY_PRESS:
+
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+
+ /* if Shift+S is pressed while hovering over a cusp node handle,
+ hold the handle in place; otherwise, process normally.
+ this handle is guaranteed not to be degenerate. */
+
+ if (held_only_shift(event->key) && _parent->_type == NODE_CUSP) {
+
+ // make opposite handle collinear,
+ // but preserve length, unless degenerate
+ if (other()->isDegenerate())
+ other()->setRelativePos(-relativePos());
+ else
+ other()->setDirection(-relativePos());
+ _parent->setType(NODE_SMOOTH, false);
+
+ // update display
+ _parent->_pm().update();
+
+ // update undo history
+ _parent->_pm()._commit(_("Change node type"));
+
+ return true;
+ }
+ break;
+
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+
+ /* if Shift+Y is pressed while hovering over a cusp, smooth, or auto node handle,
+ hold the handle in place; otherwise, process normally.
+ this handle is guaranteed not to be degenerate. */
+
+ if (held_only_shift(event->key) && (_parent->_type == NODE_CUSP ||
+ _parent->_type == NODE_SMOOTH ||
+ _parent->_type == NODE_AUTO)) {
+
+ // make opposite handle collinear, and of equal length
+ other()->setRelativePos(-relativePos());
+ _parent->setType(NODE_SYMMETRIC, false);
+
+ // update display
+ _parent->_pm().update();
+
+ // update undo history
+ _parent->_pm()._commit(_("Change node type"));
+
+ return true;
+ }
+ break;
+ }
+ break;
+
+ case GDK_2BUTTON_PRESS:
+
+ // double-click event to set the handles of a node
+ // to the position specified by DEFAULT_START_POWER
+ handle_2button_press();
+ break;
+ }
+
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+// this function moves the handle and its opposite to the position specified by DEFAULT_START_POWER
+void Handle::handle_2button_press(){
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, DEFAULT_START_POWER));
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), DEFAULT_START_POWER));
+ _pm().update();
+ }
+}
+
+bool Handle::grabbed(GdkEventMotion *)
+{
+ _saved_other_pos = other()->position();
+ _saved_length = _drag_out ? 0 : length();
+ _pm()._handleGrabbed();
+ return false;
+}
+
+void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Point parent_pos = _parent->position();
+ Geom::Point origin = _last_drag_origin();
+ SnapManager &sm = _desktop->namedview->snap_manager;
+ bool snap = held_shift(*event) ? false : sm.someSnapperMightSnap();
+ boost::optional<Inkscape::Snapper::SnapConstraint> ctrl_constraint;
+
+ // with Alt, preserve length
+ if (held_alt(*event)) {
+ new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length;
+ snap = false;
+ }
+ // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical
+ // and the original position.
+ if (held_control(*event)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+
+ // note: if snapping to the original position is only desired in the original
+ // direction of the handle, change to Ray instead of Line
+ Geom::Line original_line(parent_pos, origin);
+ Geom::Line perp_line(parent_pos, parent_pos + Geom::rot90(origin - parent_pos));
+ Geom::Point snap_pos = parent_pos + Geom::constrain_angle(
+ Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0));
+ Geom::Point orig_pos = original_line.pointAt(original_line.nearestTime(new_pos));
+ Geom::Point perp_pos = perp_line.pointAt(perp_line.nearestTime(new_pos));
+
+ Geom::Point result = snap_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - snap_pos);
+ if (Geom::distance(orig_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = orig_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - orig_pos);
+ }
+ if (Geom::distance(perp_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = perp_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - perp_pos);
+ }
+ new_pos = result;
+ // move the handle and its opposite in X fixed positions depending on parameter "steps with control"
+ // by default in live BSpline
+ if(_pm()._isBSpline()){
+ setPosition(new_pos);
+ int steps = _pm()._bsplineGetSteps();
+ new_pos=_pm()._bsplineHandleReposition(this,ceilf(_pm()._bsplineHandlePosition(this, false)*steps)/steps);
+ }
+ }
+
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ // if the snap adjustment is activated and it is not BSpline
+ if (snap && !_pm()._isBSpline()) {
+ ControlPointSelection::Set &nodes = _parent->_selection.allPoints();
+ for (auto node : nodes) {
+ Node *n = static_cast<Node*>(node);
+ unselected.push_back(n->snapCandidatePoint());
+ }
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+
+ Node *node_away = _parent->nodeAwayFrom(this);
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ Inkscape::Snapper::SnapConstraint cl(_parent->position(),
+ _parent->position() - node_away->position());
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl);
+ new_pos = p.getPoint();
+ } else if (ctrl_constraint) {
+ // NOTE: this is subtly wrong.
+ // We should get all possible constraints and snap along them using
+ // multipleConstrainedSnaps, instead of first snapping to angle and then to objects
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), *ctrl_constraint);
+ new_pos = p.getPoint();
+ } else {
+ sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE);
+ }
+ sm.unSetup();
+ }
+
+
+ // with Shift, if the node is cusp, rotate the other handle as well
+ if (_parent->type() == NODE_CUSP && !_drag_out) {
+ if (held_shift(*event)) {
+ Geom::Point other_relpos = _saved_other_pos - parent_pos;
+ other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos));
+ other()->setRelativePos(other_relpos);
+ } else {
+ // restore the position
+ other()->setPosition(_saved_other_pos);
+ }
+ }
+ // if it is BSpline, but SHIFT or CONTROL are not pressed, fix it in the original position
+ if(_pm()._isBSpline() && !held_shift(*event) && !held_control(*event)){
+ new_pos=_last_drag_origin();
+ }
+ move(new_pos); // needed for correct update, even though it's redundant
+ _pm().update();
+}
+
+void Handle::ungrabbed(GdkEventButton *event)
+{
+ // hide the handle if it's less than dragtolerance away from the node
+ // however, never do this for cancelled drag / broken grab
+ // TODO is this actually a good idea?
+ if (event) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position());
+ if (dist.length() <= drag_tolerance) {
+ move(_parent->position());
+ }
+ }
+
+ // HACK: If the handle was dragged out, call parent's ungrabbed handler,
+ // so that transform handles reappear
+ if (_drag_out) {
+ _parent->ungrabbed(event);
+ }
+ _drag_out = false;
+
+ _pm()._handleUngrabbed();
+}
+
+bool Handle::clicked(GdkEventButton *event)
+{
+ _pm()._handleClicked(this, event);
+ return true;
+}
+
+Handle const *Handle::other() const
+{
+ return const_cast<Handle *>(this)->other();
+}
+
+Handle *Handle::other()
+{
+ if (this == &_parent->_front) {
+ return &_parent->_back;
+ } else {
+ return &_parent->_front;
+ }
+}
+
+static double snap_increment_degrees() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ return 180.0 / snaps;
+}
+
+Glib::ustring Handle::_getTip(unsigned state) const
+{
+ /* a trick to mark as BSpline if the node has no strength;
+ we are going to use it later to show the appropriate messages.
+ we cannot do it in any different way because the function is constant. */
+ Handle *h = const_cast<Handle *>(this);
+ bool isBSpline = _pm()._isBSpline();
+ bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate();
+ Glib::ustring s = C_("Path handle tip",
+ "node control handle"); // not expected
+
+ if (state_held_alt(state) && !isBSpline) {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = format_tip(C_("Path handle tip",
+ "<b>Shift+Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments, "
+ "and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else {
+ s = format_tip(C_("Path handle tip",
+ "<b>Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments"),
+ snap_increment_degrees());
+ }
+ }
+ else {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = C_("Path handle tip",
+ "<b>Shift+Alt</b>: preserve handle length and rotate both handles");
+ }
+ else {
+ s = C_("Path handle tip",
+ "<b>Alt</b>: preserve handle length while dragging");
+ }
+ }
+ }
+ else {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = format_tip(C_("Path handle tip",
+ "<b>Shift+Ctrl</b>: "
+ "snap rotation angle to %g° increments, and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else if (isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "Snap handle to steps defined in BSpline Live Path Effect");
+ }
+ else {
+ s = format_tip(C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "snap rotation angle to %g° increments, click to retract"),
+ snap_increment_degrees());
+ }
+ }
+ else if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: rotate both handles by the same angle");
+ }
+ else if (state_held_shift(state) && isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: move handle");
+ }
+ else {
+ char const *handletype = handle_type_to_localized_string(_parent->_type);
+ char const *more;
+
+ if (can_shift_rotate && !isBSpline) {
+ more = C_("Path handle tip",
+ "Shift, Ctrl, Alt");
+ }
+ else if (isBSpline) {
+ more = C_("Path handle tip",
+ "Ctrl");
+ }
+ else {
+ more = C_("Path handle tip",
+ "Ctrl, Alt");
+ }
+
+ if (_parent->type() == NODE_CUSP) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+S to make smooth" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SMOOTH) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_AUTO) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to make smooth, "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SYMMETRIC) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (isBSpline) {
+ double power = _pm()._bsplineHandlePosition(h);
+ s = format_tip(C_("Path handle tip",
+ "<b>BSpline node handle</b> (%.3g power): "
+ "Shift-drag to move, "
+ "double-click to reset. "
+ "(more: %s)"),
+ power, more);
+ }
+ else {
+ s = C_("Path handle tip",
+ "<b>unknown node handle</b>"); // not expected
+ }
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+ // report angle in mathematical convention
+ double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position());
+ angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360
+ angle *= 360.0 / (2 * M_PI);
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Inkscape::Util::Quantity len_q = Inkscape::Util::Quantity(length(), "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring len = len_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Path handle tip",
+ "Move handle by %s, %s; angle %.2f°, length %s"), x.c_str(), y.c_str(), angle, len.c_str());
+ return ret;
+}
+
+Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) :
+ SelectableControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER,
+ CTRL_TYPE_NODE_CUSP,
+ *data.selection,
+ node_colors, data.node_group),
+ _front(data, initial_pos, this),
+ _back(data, initial_pos, this),
+ _type(NODE_CUSP),
+ _handles_shown(false)
+{
+ // NOTE we do not set type here, because the handles are still degenerate
+}
+
+Node const *Node::_next() const
+{
+ return const_cast<Node*>(this)->_next();
+}
+
+// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined.
+Node *Node::_next()
+{
+ NodeList::iterator n = NodeList::get_iterator(this).next();
+ if (n) {
+ return n.ptr();
+ } else {
+ return nullptr;
+ }
+}
+
+Node const *Node::_prev() const
+{
+ return const_cast<Node *>(this)->_prev();
+}
+
+Node *Node::_prev()
+{
+ NodeList::iterator p = NodeList::get_iterator(this).prev();
+ if (p) {
+ return p.ptr();
+ } else {
+ return nullptr;
+ }
+}
+
+void Node::move(Geom::Point const &new_pos)
+{
+ // move handles when the node moves.
+ Geom::Point old_pos = position();
+ Geom::Point delta = new_pos - position();
+
+ // save the previous nodes strength to apply it again once the node is moved
+ double nodeWeight = NO_POWER;
+ double nextNodeWeight = NO_POWER;
+ double prevNodeWeight = NO_POWER;
+ Node *n = this;
+ Node * nextNode = n->nodeToward(n->front());
+ Node * prevNode = n->nodeToward(n->back());
+ nodeWeight = fmax(_pm()._bsplineHandlePosition(n->front(), false),_pm()._bsplineHandlePosition(n->back(), false));
+ if(prevNode){
+ prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front());
+ }
+ if(nextNode){
+ nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
+ }
+
+ setPosition(new_pos);
+
+ _front.setPosition(_front.position() + delta);
+ _back.setPosition(_back.position() + delta);
+
+ // if the node has a smooth handle after a line segment, it should be kept collinear
+ // with the segment
+ _fixNeighbors(old_pos, new_pos);
+
+ // move the affected handles. First the node ones, later the adjoining ones.
+ if(_pm()._isBSpline()){
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(),nodeWeight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(),nodeWeight));
+ if(prevNode){
+ prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight));
+ }
+ if(nextNode){
+ nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight));
+ }
+ }
+ Inkscape::UI::Tools::sp_update_helperpath();
+}
+
+void Node::transform(Geom::Affine const &m)
+{
+
+ Geom::Point old_pos = position();
+
+ // save the previous nodes strength to apply it again once the node is moved
+ double nodeWeight = NO_POWER;
+ double nextNodeWeight = NO_POWER;
+ double prevNodeWeight = NO_POWER;
+ Node *n = this;
+ Node * nextNode = n->nodeToward(n->front());
+ Node * prevNode = n->nodeToward(n->back());
+ nodeWeight = _pm()._bsplineHandlePosition(n->front());
+ if(prevNode){
+ prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front());
+ }
+ if(nextNode){
+ nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
+ }
+
+ setPosition(position() * m);
+ _front.setPosition(_front.position() * m);
+ _back.setPosition(_back.position() * m);
+
+ /* Affine transforms keep handle invariants for smooth and symmetric nodes,
+ * but smooth nodes at ends of linear segments and auto nodes need special treatment */
+ _fixNeighbors(old_pos, position());
+
+ // move the involved handles. First the node ones, later the adjoining ones.
+ if(_pm()._isBSpline()){
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(), nodeWeight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(), nodeWeight));
+ if(prevNode){
+ prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight));
+ }
+ if(nextNode){
+ nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight));
+ }
+ }
+}
+
+Geom::Rect Node::bounds() const
+{
+ Geom::Rect b(position(), position());
+ b.expandTo(_front.position());
+ b.expandTo(_back.position());
+ return b;
+}
+
+void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos)
+{
+ // This method restores handle invariants for neighboring nodes,
+ // and invariants that are based on positions of those nodes for this one.
+
+ // Fix auto handles
+ if (_type == NODE_AUTO) _updateAutoHandles();
+ if (old_pos != new_pos) {
+ if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles();
+ if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles();
+ }
+
+ /* Fix smooth handles at the ends of linear segments.
+ Rotate the appropriate handle to be collinear with the segment.
+ If there is a smooth node at the other end of the segment, rotate it too. */
+ Handle *handle, *other_handle;
+ Node *other;
+ if (_is_line_segment(this, _next())) {
+ handle = &_back;
+ other = _next();
+ other_handle = &_next()->_front;
+ } else if (_is_line_segment(_prev(), this)) {
+ handle = &_front;
+ other = _prev();
+ other_handle = &_prev()->_back;
+ } else return;
+
+ if (_type == NODE_SMOOTH && !handle->isDegenerate()) {
+ handle->setDirection(other->position(), new_pos);
+ }
+ // also update the handle on the other end of the segment
+ if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) {
+ other_handle->setDirection(new_pos, other->position());
+ }
+}
+
+void Node::_updateAutoHandles()
+{
+ // Recompute the position of automatic handles. For endnodes, retract both handles.
+ // (It's only possible to create an end auto node through the XML editor.)
+ if (isEndNode()) {
+ _front.retract();
+ _back.retract();
+ return;
+ }
+
+ // auto nodes automatically adjust their handles to give
+ // an appearance of smoothness, no matter what their surroundings are.
+ Geom::Point vec_next = _next()->position() - position();
+ Geom::Point vec_prev = _prev()->position() - position();
+ double len_next = vec_next.length(), len_prev = vec_prev.length();
+ if (len_next > 0 && len_prev > 0) {
+ // "dir" is an unit vector perpendicular to the bisector of the angle created
+ // by the previous node, this auto node and the next node.
+ Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
+ // Handle lengths are equal to 1/3 of the distance from the adjacent node.
+ _back.setRelativePos(-dir * (len_prev / 3));
+ _front.setRelativePos(dir * (len_next / 3));
+ } else {
+ // If any of the adjacent nodes coincides, retract both handles.
+ _front.retract();
+ _back.retract();
+ }
+}
+
+void Node::showHandles(bool v)
+{
+ _handles_shown = v;
+ if (!_front.isDegenerate()) {
+ _front.setVisible(v);
+ }
+ if (!_back.isDegenerate()) {
+ _back.setVisible(v);
+ }
+
+}
+
+void Node::updateHandles()
+{
+ _handleControlStyling();
+
+ _front._handleControlStyling();
+ _back._handleControlStyling();
+}
+
+
+void Node::setType(NodeType type, bool update_handles)
+{
+ if (type == NODE_PICK_BEST) {
+ pickBestType();
+ updateState(); // The size of the control might have changed
+ return;
+ }
+
+ // if update_handles is true, adjust handle positions to match the node type
+ // handle degenerate handles appropriately
+ if (update_handles) {
+ switch (type) {
+ case NODE_CUSP:
+ // nothing to do
+ break;
+ case NODE_AUTO:
+ // auto handles make no sense for endnodes
+ if (isEndNode()) return;
+ _updateAutoHandles();
+ break;
+ case NODE_SMOOTH: {
+ // ignore attempts to make smooth endnodes.
+ if (isEndNode()) return;
+ // rotate handles to be collinear
+ // for degenerate nodes set positions like auto handles
+ bool prev_line = _is_line_segment(_prev(), this);
+ bool next_line = _is_line_segment(this, _next());
+ if (_type == NODE_SMOOTH) {
+ // For a node that is already smooth and has a degenerate handle,
+ // drag out the second handle without changing the direction of the first one.
+ if (_front.isDegenerate()) {
+ double dist = Geom::distance(_next()->position(), position());
+ _front.setRelativePos(Geom::unit_vector(-_back.relativePos()) * dist / 3);
+ }
+ if (_back.isDegenerate()) {
+ double dist = Geom::distance(_prev()->position(), position());
+ _back.setRelativePos(Geom::unit_vector(-_front.relativePos()) * dist / 3);
+ }
+ } else if (isDegenerate()) {
+ _updateAutoHandles();
+ } else if (_front.isDegenerate()) {
+ // if the front handle is degenerate and next path segment is a line, make back collinear;
+ // otherwise, pull out the other handle to 1/3 of distance to prev.
+ if (next_line) {
+ _back.setDirection(*_next(), *this);
+ } else if (_prev()) {
+ Geom::Point dir = direction(_back, *this);
+ _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir);
+ }
+ } else if (_back.isDegenerate()) {
+ if (prev_line) {
+ _front.setDirection(*_prev(), *this);
+ } else if (_next()) {
+ Geom::Point dir = direction(_front, *this);
+ _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir);
+ }
+ } else {
+ /* both handles are extended. make collinear while keeping length.
+ first make back collinear with the vector front ---> back,
+ then make front collinear with back ---> node.
+ (not back ---> front, because back's position was changed in the first call) */
+ _back.setDirection(_front, _back);
+ _front.setDirection(_back, *this);
+ }
+ } break;
+ case NODE_SYMMETRIC:
+ if (isEndNode()) return; // symmetric handles make no sense for endnodes
+ if (isDegenerate()) {
+ // similar to auto handles but set the same length for both
+ Geom::Point vec_next = _next()->position() - position();
+ Geom::Point vec_prev = _prev()->position() - position();
+ double len_next = vec_next.length(), len_prev = vec_prev.length();
+ double len = (len_next + len_prev) / 6; // take 1/3 of average
+ if (len == 0) return;
+
+ Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev);
+ _back.setRelativePos(-dir * len);
+ _front.setRelativePos(dir * len);
+ } else {
+ // Both handles are extended. Compute average length, use direction from
+ // back handle to front handle. This also works correctly for degenerates
+ double len = (_front.length() + _back.length()) / 2;
+ Geom::Point dir = direction(_back, _front);
+ _front.setRelativePos(dir * len);
+ _back.setRelativePos(-dir * len);
+ }
+ break;
+ default: break;
+ }
+ // in node type changes, for BSpline traces, we can either maintain them
+ // with NO_POWER power in border mode, or give them the default power in curve mode.
+ if(_pm()._isBSpline()){
+ double weight = NO_POWER;
+ if(_pm()._bsplineHandlePosition(this->front()) != NO_POWER ){
+ weight = DEFAULT_START_POWER;
+ }
+ _front.setPosition(_pm()._bsplineHandleReposition(this->front(), weight));
+ _back.setPosition(_pm()._bsplineHandleReposition(this->back(), weight));
+ }
+ }
+ _type = type;
+ _setControlType(nodeTypeToCtrlType(_type));
+ updateState();
+}
+
+void Node::pickBestType()
+{
+ _type = NODE_CUSP;
+ bool front_degen = _front.isDegenerate();
+ bool back_degen = _back.isDegenerate();
+ bool both_degen = front_degen && back_degen;
+ bool neither_degen = !front_degen && !back_degen;
+ do {
+ // if both handles are degenerate, do nothing
+ if (both_degen) break;
+ // if neither are degenerate, check their respective positions
+ if (neither_degen) {
+ // for now do not automatically make nodes symmetric, it can be annoying
+ /*if (Geom::are_near(front_delta, -back_delta)) {
+ _type = NODE_SYMMETRIC;
+ break;
+ }*/
+ if (are_collinear_within_serializing_error(_front.position(), position(), _back.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ }
+ // check whether the handle aligns with the previous line segment.
+ // we know that if front is degenerate, back isn't, because
+ // both_degen was false
+ if (front_degen && _next() && _next()->_back.isDegenerate()) {
+ if (are_collinear_within_serializing_error(_next()->position(), position(), _back.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) {
+ if (are_collinear_within_serializing_error(_prev()->position(), position(), _front.position())) {
+ _type = NODE_SMOOTH;
+ break;
+ }
+ }
+ } while (false);
+ _setControlType(nodeTypeToCtrlType(_type));
+ updateState();
+}
+
+bool Node::isEndNode() const
+{
+ return !_prev() || !_next();
+}
+
+void Node::sink()
+{
+ sp_canvas_item_move_to_z(_canvas_item, 0);
+}
+
+NodeType Node::parse_nodetype(char x)
+{
+ switch (x) {
+ case 'a': return NODE_AUTO;
+ case 'c': return NODE_CUSP;
+ case 's': return NODE_SMOOTH;
+ case 'z': return NODE_SYMMETRIC;
+ default: return NODE_PICK_BEST;
+ }
+}
+
+bool Node::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ int dir = 0;
+
+ switch (event->type)
+ {
+ case GDK_SCROLL:
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ dir = 1;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ dir = -1;
+ } else if (event->scroll.direction == GDK_SCROLL_SMOOTH) {
+ dir = event->scroll.delta_y > 0 ? -1 : 1;
+ } else {
+ break;
+ }
+ if (held_control(event->scroll)) {
+ _linearGrow(dir);
+ } else {
+ _selection.spatialGrow(this, dir);
+ }
+ return true;
+ case GDK_KEY_PRESS:
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_Page_Up:
+ dir = 1;
+ break;
+ case GDK_KEY_Page_Down:
+ dir = -1;
+ break;
+ default: goto bail_out;
+ }
+
+ if (held_control(event->key)) {
+ _linearGrow(dir);
+ } else {
+ _selection.spatialGrow(this, dir);
+ }
+ return true;
+
+ default:
+ break;
+ }
+
+ bail_out:
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+void Node::_linearGrow(int dir)
+{
+ // Interestingly, we do not need any help from PathManipulator when doing linear grow.
+ // First handle the trivial case of growing over an unselected node.
+ if (!selected() && dir > 0) {
+ _selection.insert(this);
+ return;
+ }
+
+ NodeList::iterator this_iter = NodeList::get_iterator(this);
+ NodeList::iterator fwd = this_iter, rev = this_iter;
+ double distance_back = 0, distance_front = 0;
+
+ // Linear grow is simple. We find the first unselected nodes in each direction
+ // and compare the linear distances to them.
+ if (dir > 0) {
+ if (!selected()) {
+ _selection.insert(this);
+ return;
+ }
+
+ // find first unselected nodes on both sides
+ while (fwd && fwd->selected()) {
+ NodeList::iterator n = fwd.next();
+ distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ if (fwd == this_iter)
+ // there is no unselected node in this cyclic subpath
+ return;
+ }
+ // do the same for the second direction. Do not check for equality with
+ // this node, because there is at least one unselected node in the subpath,
+ // so we are guaranteed to stop.
+ while (rev && rev->selected()) {
+ NodeList::iterator p = rev.prev();
+ distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+
+ NodeList::iterator t; // node to select
+ if (fwd && rev) {
+ if (distance_front <= distance_back) t = fwd;
+ else t = rev;
+ } else {
+ if (fwd) t = fwd;
+ if (rev) t = rev;
+ }
+ if (t) _selection.insert(t.ptr());
+
+ // Linear shrink is more complicated. We need to find the farthest selected node.
+ // This means we have to check the entire subpath. We go in the direction in which
+ // the distance we traveled is lower. We do this until we run out of nodes (ends of path)
+ // or the two iterators meet. On the way, we store the last selected node and its distance
+ // in each direction (if any). At the end, we choose the one that is farther and deselect it.
+ } else {
+ // both iterators that store last selected nodes are initially empty
+ NodeList::iterator last_fwd, last_rev;
+ double last_distance_back = 0, last_distance_front = 0;
+
+ while (rev || fwd) {
+ if (fwd && (!rev || distance_front <= distance_back)) {
+ if (fwd->selected()) {
+ last_fwd = fwd;
+ last_distance_front = distance_front;
+ }
+ NodeList::iterator n = fwd.next();
+ if (n) distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ } else if (rev && (!fwd || distance_front > distance_back)) {
+ if (rev->selected()) {
+ last_rev = rev;
+ last_distance_back = distance_back;
+ }
+ NodeList::iterator p = rev.prev();
+ if (p) distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+ // Check whether we walked the entire cyclic subpath.
+ // This is initially true because both iterators start from this node,
+ // so this check cannot go in the while condition.
+ // When this happens, we need to check the last node, pointed to by the iterators.
+ if (fwd && fwd == rev) {
+ if (!fwd->selected()) break;
+ NodeList::iterator fwdp = fwd.prev(), revn = rev.next();
+ double df = distance_front + Geom::bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd);
+ double db = distance_back + Geom::bezier_length(*revn, revn->_back, rev->_front, *rev);
+ if (df > db) {
+ last_fwd = fwd;
+ last_distance_front = df;
+ } else {
+ last_rev = rev;
+ last_distance_back = db;
+ }
+ break;
+ }
+ }
+
+ NodeList::iterator t;
+ if (last_fwd && last_rev) {
+ if (last_distance_front >= last_distance_back) t = last_fwd;
+ else t = last_rev;
+ } else {
+ if (last_fwd) t = last_fwd;
+ if (last_rev) t = last_rev;
+ }
+ if (t) _selection.erase(t.ptr());
+ }
+}
+
+void Node::_setState(State state)
+{
+ // change node size to match type and selection state
+ ControlManager &mgr = ControlManager::getManager();
+ mgr.setSelected(_canvas_item, selected());
+ switch (state) {
+ case STATE_NORMAL:
+ mgr.setActive(_canvas_item, false);
+ mgr.setPrelight(_canvas_item, false);
+ break;
+ case STATE_MOUSEOVER:
+ mgr.setActive(_canvas_item, false);
+ mgr.setPrelight(_canvas_item, true);
+ break;
+ case STATE_CLICKED:
+ mgr.setActive(_canvas_item, true);
+ mgr.setPrelight(_canvas_item, false);
+ // show the handles when selecting the nodes
+ if(_pm()._isBSpline()){
+ this->front()->setPosition(_pm()._bsplineHandleReposition(this->front()));
+ this->back()->setPosition(_pm()._bsplineHandleReposition(this->back()));
+ }
+ break;
+ }
+ SelectableControlPoint::_setState(state);
+}
+
+bool Node::grabbed(GdkEventMotion *event)
+{
+ if (SelectableControlPoint::grabbed(event)) {
+ return true;
+ }
+
+ // Dragging out handles with Shift + drag on a node.
+ if (!held_shift(*event)) {
+ return false;
+ }
+
+ Geom::Point evp = event_point(*event);
+ Geom::Point rel_evp = evp - _last_click_event_point();
+
+ // This should work even if dragtolerance is zero and evp coincides with node position.
+ double angle_next = HUGE_VAL;
+ double angle_prev = HUGE_VAL;
+ bool has_degenerate = false;
+ // determine which handle to drag out based on degeneration and the direction of drag
+ if (_front.isDegenerate() && _next()) {
+ Geom::Point next_relpos = _desktop->d2w(_next()->position())
+ - _desktop->d2w(position());
+ angle_next = fabs(Geom::angle_between(rel_evp, next_relpos));
+ has_degenerate = true;
+ }
+ if (_back.isDegenerate() && _prev()) {
+ Geom::Point prev_relpos = _desktop->d2w(_prev()->position())
+ - _desktop->d2w(position());
+ angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos));
+ has_degenerate = true;
+ }
+ if (!has_degenerate) {
+ return false;
+ }
+
+ Handle *h = angle_next < angle_prev ? &_front : &_back;
+
+ h->setPosition(_desktop->w2d(evp));
+ h->setVisible(true);
+ h->transferGrab(this, event);
+ Handle::_drag_out = true;
+ return true;
+}
+
+void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ // For a note on how snapping is implemented in Inkscape, see snap.h.
+ SnapManager &sm = _desktop->namedview->snap_manager;
+ // even if we won't really snap, we might still call the one of the
+ // constrainedSnap() methods to enforce the constraints, so we need
+ // to setup the snapmanager anyway; this is also required for someSnapperMightSnap()
+ sm.setup(_desktop);
+
+ // do not snap when Shift is pressed
+ bool snap = !held_shift(*event) && sm.someSnapperMightSnap();
+
+ Inkscape::SnappedPoint sp;
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ if (snap) {
+ /* setup
+ * TODO We are doing this every time a snap happens. It should once be done only once
+ * per drag - maybe in the grabbed handler?
+ * TODO Unselected nodes vector must be valid during the snap run, because it is not
+ * copied. Fix this in snap.h and snap.cpp, then the above.
+ * TODO Snapping to unselected segments of selected paths doesn't work yet. */
+
+ // Build the list of unselected nodes.
+ typedef ControlPointSelection::Set Set;
+ Set &nodes = _selection.allPoints();
+ for (auto node : nodes) {
+ if (!node->selected()) {
+ Node *n = static_cast<Node*>(node);
+ Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType());
+ unselected.push_back(p);
+ }
+ }
+ sm.unSetup();
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+ }
+
+ // Snap candidate point for free snapping; this will consider snapping tangentially
+ // and perpendicularly and therefore the origin or direction vector must be set
+ Inkscape::SnapCandidatePoint scp_free(new_pos, _snapSourceType());
+
+ boost::optional<Geom::Point> front_point, back_point;
+ Geom::Point origin = _last_drag_origin();
+ Geom::Point dummy_cp;
+ if (_front.isDegenerate()) {
+ if (_is_line_segment(this, _next())) {
+ front_point = _next()->position() - origin;
+ if (_next()->selected()) {
+ dummy_cp = _next()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _next()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else {
+ front_point = _front.relativePos();
+ scp_free.addVector(*front_point);
+ }
+ if (_back.isDegenerate()) {
+ if (_is_line_segment(_prev(), this)) {
+ back_point = _prev()->position() - origin;
+ if (_prev()->selected()) {
+ dummy_cp = _prev()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _prev()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else {
+ back_point = _back.relativePos();
+ scp_free.addVector(*back_point);
+ }
+
+ if (held_control(*event)) {
+ // We're about to consider a constrained snap, which is already limited to 1D
+ // Therefore tangential or perpendicular snapping will not be considered, and therefore
+ // all calls above to scp_free.addVector() and scp_free.addOrigin() can be neglected
+ std::vector<Inkscape::Snapper::SnapConstraint> constraints;
+ if (held_alt(*event)) {
+ // with Ctrl+Alt, constrain to handle lines
+ // project the new position onto a handle line that is closer;
+ // also snap to perpendiculars of handle lines
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double min_angle = M_PI / snaps;
+
+ boost::optional<Geom::Point> fperp_point, bperp_point;
+ if (front_point) {
+ constraints.emplace_back(origin, *front_point);
+ fperp_point = Geom::rot90(*front_point);
+ }
+ if (back_point) {
+ constraints.emplace_back(origin, *back_point);
+ bperp_point = Geom::rot90(*back_point);
+ }
+ // perpendiculars only snap when they are further than snap increment away
+ // from the second handle constraint
+ if (fperp_point && (!back_point ||
+ (fabs(Geom::angle_between(*fperp_point, *back_point)) > min_angle &&
+ fabs(Geom::angle_between(*fperp_point, *back_point)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *fperp_point);
+ }
+ if (bperp_point && (!front_point ||
+ (fabs(Geom::angle_between(*bperp_point, *front_point)) > min_angle &&
+ fabs(Geom::angle_between(*bperp_point, *front_point)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *bperp_point);
+ }
+
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ } else {
+ // with Ctrl, constrain to axes
+ constraints.emplace_back(origin, Geom::Point(1, 0));
+ constraints.emplace_back(origin, Geom::Point(0, 1));
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ }
+ new_pos = sp.getPoint();
+ } else if (snap) {
+ Inkscape::SnappedPoint sp = sm.freeSnap(scp_free);
+ new_pos = sp.getPoint();
+ }
+
+ sm.unSetup();
+
+ SelectableControlPoint::dragged(new_pos, event);
+}
+
+bool Node::clicked(GdkEventButton *event)
+{
+ if(_pm()._nodeClicked(this, event))
+ return true;
+ return SelectableControlPoint::clicked(event);
+}
+
+Inkscape::SnapSourceType Node::_snapSourceType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPSOURCE_NODE_SMOOTH;
+ return SNAPSOURCE_NODE_CUSP;
+}
+Inkscape::SnapTargetType Node::_snapTargetType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPTARGET_NODE_SMOOTH;
+ return SNAPTARGET_NODE_CUSP;
+}
+
+Inkscape::SnapCandidatePoint Node::snapCandidatePoint()
+{
+ return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType());
+}
+
+Handle *Node::handleToward(Node *to)
+{
+ if (_next() == to) {
+ return front();
+ }
+ if (_prev() == to) {
+ return back();
+ }
+ g_error("Node::handleToward(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeToward(Handle *dir)
+{
+ if (front() == dir) {
+ return _next();
+ }
+ if (back() == dir) {
+ return _prev();
+ }
+ g_error("Node::nodeToward(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Handle *Node::handleAwayFrom(Node *to)
+{
+ if (_next() == to) {
+ return back();
+ }
+ if (_prev() == to) {
+ return front();
+ }
+ g_error("Node::handleAwayFrom(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeAwayFrom(Handle *h)
+{
+ if (front() == h) {
+ return _prev();
+ }
+ if (back() == h) {
+ return _next();
+ }
+ g_error("Node::nodeAwayFrom(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Glib::ustring Node::_getTip(unsigned state) const
+{
+ bool isBSpline = _pm()._isBSpline();
+ Handle *h = const_cast<Handle *>(&_front);
+ Glib::ustring s = C_("Path node tip",
+ "node handle"); // not expected
+
+ if (state_held_shift(state)) {
+ bool can_drag_out = (_next() && _front.isDegenerate()) ||
+ (_prev() && _back.isDegenerate());
+
+ if (can_drag_out) {
+ /*if (state_held_control(state)) {
+ s = format_tip(C_("Path node tip",
+ "<b>Shift+Ctrl:</b> drag out a handle and snap its angle "
+ "to %f° increments"), snap_increment_degrees());
+ }*/
+ s = C_("Path node tip",
+ "<b>Shift</b>: drag out a handle, click to toggle selection");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Shift</b>: click to toggle selection");
+ }
+ }
+
+ else if (state_held_control(state)) {
+ if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Ctrl+Alt</b>: move along handle lines, click to delete node");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Ctrl</b>: move along axes, click to change node type");
+ }
+ }
+
+ else if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Alt</b>: sculpt nodes");
+ }
+
+ else { // No modifiers: assemble tip from node type
+ char const *nodetype = node_type_to_localized_string(_type);
+ double power = _pm()._bsplineHandlePosition(h);
+
+ if (_selection.transformHandlesEnabled() && selected()) {
+ if (_selection.size() == 1) {
+ if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to toggle scale/rotation handles" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ }
+ else if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), x.c_str(), y.c_str());
+ return ret;
+}
+
+/**
+ * See also: Handle::handle_type_to_localized_string(NodeType type)
+ */
+char const *Node::node_type_to_localized_string(NodeType type)
+{
+ switch (type) {
+ case NODE_CUSP:
+ return _("Corner node");
+ case NODE_SMOOTH:
+ return _("Smooth node");
+ case NODE_SYMMETRIC:
+ return _("Symmetric node");
+ case NODE_AUTO:
+ return _("Auto-smooth node");
+ default:
+ return "";
+ }
+}
+
+bool Node::_is_line_segment(Node *first, Node *second)
+{
+ if (!first || !second) return false;
+ if (first->_next() == second)
+ return first->_front.isDegenerate() && second->_back.isDegenerate();
+ if (second->_next() == first)
+ return second->_front.isDegenerate() && first->_back.isDegenerate();
+ return false;
+}
+
+NodeList::NodeList(SubpathList &splist)
+ : _list(splist)
+ , _closed(false)
+{
+ this->ln_list = this;
+ this->ln_next = this;
+ this->ln_prev = this;
+}
+
+NodeList::~NodeList()
+{
+ clear();
+}
+
+bool NodeList::empty()
+{
+ return ln_next == this;
+}
+
+NodeList::size_type NodeList::size()
+{
+ size_type sz = 0;
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz;
+ return sz;
+}
+
+bool NodeList::closed()
+{
+ return _closed;
+}
+
+bool NodeList::degenerate()
+{
+ return closed() ? empty() : ++begin() == end();
+}
+
+NodeList::iterator NodeList::before(double t, double *fracpart)
+{
+ double intpart;
+ *fracpart = std::modf(t, &intpart);
+ int index = intpart;
+
+ iterator ret = begin();
+ std::advance(ret, index);
+ return ret;
+}
+
+NodeList::iterator NodeList::before(Geom::PathTime const &pvp)
+{
+ iterator ret = begin();
+ std::advance(ret, pvp.curve_index);
+ return ret;
+}
+
+NodeList::iterator NodeList::insert(iterator pos, Node *x)
+{
+ ListNode *ins = pos._node;
+ x->ln_next = ins;
+ x->ln_prev = ins->ln_prev;
+ ins->ln_prev->ln_next = x;
+ ins->ln_prev = x;
+ x->ln_list = this;
+ return iterator(x);
+}
+
+void NodeList::splice(iterator pos, NodeList &list)
+{
+ splice(pos, list, list.begin(), list.end());
+}
+
+void NodeList::splice(iterator pos, NodeList &list, iterator i)
+{
+ NodeList::iterator j = i;
+ ++j;
+ splice(pos, list, i, j);
+}
+
+void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last)
+{
+ ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node;
+ for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) {
+ ln->ln_list = this;
+ }
+ ins_beg->ln_prev->ln_next = ins_end;
+ ins_end->ln_prev->ln_next = at;
+ at->ln_prev->ln_next = ins_beg;
+
+ ListNode *atprev = at->ln_prev;
+ at->ln_prev = ins_end->ln_prev;
+ ins_end->ln_prev = ins_beg->ln_prev;
+ ins_beg->ln_prev = atprev;
+}
+
+void NodeList::shift(int n)
+{
+ // 1. make the list perfectly cyclic
+ ln_next->ln_prev = ln_prev;
+ ln_prev->ln_next = ln_next;
+ // 2. find new begin
+ ListNode *new_begin = ln_next;
+ if (n > 0) {
+ for (; n > 0; --n) new_begin = new_begin->ln_next;
+ } else {
+ for (; n < 0; ++n) new_begin = new_begin->ln_prev;
+ }
+ // 3. relink begin to list
+ ln_next = new_begin;
+ ln_prev = new_begin->ln_prev;
+ new_begin->ln_prev->ln_next = this;
+ new_begin->ln_prev = this;
+}
+
+void NodeList::reverse()
+{
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) {
+ std::swap(ln->ln_next, ln->ln_prev);
+ Node *node = static_cast<Node*>(ln);
+ Geom::Point save_pos = node->front()->position();
+ node->front()->setPosition(node->back()->position());
+ node->back()->setPosition(save_pos);
+ }
+ std::swap(ln_next, ln_prev);
+}
+
+void NodeList::clear()
+{
+ // ugly but more efficient clearing mechanism
+ std::vector<ControlPointSelection *> to_clear;
+ std::vector<std::pair<SelectableControlPoint *, long> > nodes;
+ long in = -1;
+ for (iterator i = begin(); i != end(); ++i) {
+ SelectableControlPoint *rm = static_cast<Node*>(i._node);
+ if (std::find(to_clear.begin(), to_clear.end(), &rm->_selection) == to_clear.end()) {
+ to_clear.push_back(&rm->_selection);
+ ++in;
+ }
+ nodes.emplace_back(rm, in);
+ }
+ for (size_t i = 0, e = nodes.size(); i != e; ++i) {
+ to_clear[nodes[i].second]->erase(nodes[i].first, false);
+ }
+ std::vector<std::vector<SelectableControlPoint *> > emission;
+ for (long i = 0, e = to_clear.size(); i != e; ++i) {
+ emission.emplace_back();
+ for (size_t j = 0, f = nodes.size(); j != f; ++j) {
+ if (nodes[j].second != i)
+ break;
+ emission[i].push_back(nodes[j].first);
+ }
+ }
+
+ for (size_t i = 0, e = emission.size(); i != e; ++i) {
+ to_clear[i]->signal_selection_changed.emit(emission[i], false);
+ }
+
+ for (iterator i = begin(); i != end();)
+ erase (i++);
+}
+
+NodeList::iterator NodeList::erase(iterator i)
+{
+ // some gymnastics are required to ensure that the node is valid when deleted;
+ // otherwise the code that updates handle visibility will break
+ Node *rm = static_cast<Node*>(i._node);
+ ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev;
+ ++i;
+ delete rm;
+ rmprev->ln_next = rmnext;
+ rmnext->ln_prev = rmprev;
+ return i;
+}
+
+// TODO this method is very ugly!
+// converting SubpathList to an intrusive list might allow us to get rid of it
+void NodeList::kill()
+{
+ for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) {
+ if (i->get() == this) {
+ _list.erase(i);
+ return;
+ }
+ }
+}
+
+NodeList &NodeList::get(Node *n) {
+ return n->nodeList();
+}
+NodeList &NodeList::get(iterator const &i) {
+ return *(i._node->ln_list);
+}
+
+
+} // 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/tool/node.h b/src/ui/tool/node.h
new file mode 100644
index 0000000..d4e09ca
--- /dev/null
+++ b/src/ui/tool/node.h
@@ -0,0 +1,527 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Editable node and associated data structures.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_NODE_H
+#define SEEN_UI_TOOL_NODE_H
+
+#include <iterator>
+#include <iosfwd>
+#include <stdexcept>
+#include <cstddef>
+#include <functional>
+
+#include "ui/tool/selectable-control-point.h"
+#include "snapped-point.h"
+#include "ui/tool/node-types.h"
+
+struct SPCtrlLine;
+
+namespace Inkscape {
+namespace UI {
+template <typename> class NodeIterator;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+
+class Node;
+class Handle;
+class NodeList;
+class SubpathList;
+template <typename> class NodeIterator;
+
+std::ostream &operator<<(std::ostream &, NodeType);
+
+/*
+template <typename T>
+struct ListMember {
+ T *next;
+ T *prev;
+};
+struct SubpathMember : public ListMember<NodeListMember> {
+ Subpath *list;
+};
+struct SubpathListMember : public ListMember<SubpathListMember> {
+ SubpathList *list;
+};
+*/
+
+struct ListNode {
+ ListNode *ln_next;
+ ListNode *ln_prev;
+ NodeList *ln_list;
+};
+
+struct NodeSharedData {
+ SPDesktop *desktop;
+ ControlPointSelection *selection;
+ SPCanvasGroup *node_group;
+ SPCanvasGroup *handle_group;
+ SPCanvasGroup *handle_line_group;
+};
+
+class Handle : public ControlPoint {
+public:
+
+ ~Handle() override;
+ inline Geom::Point relativePos() const;
+ inline double length() const;
+ bool isDegenerate() const { return _degenerate; } // True if the handle is retracted, i.e. has zero length.
+
+ void setVisible(bool) override;
+ void move(Geom::Point const &p) override;
+
+ void setPosition(Geom::Point const &p) override;
+ inline void setRelativePos(Geom::Point const &p);
+ void setLength(double len);
+ void retract();
+ void setDirection(Geom::Point const &from, Geom::Point const &to);
+ void setDirection(Geom::Point const &dir);
+ Node *parent() { return _parent; }
+ Handle *other();
+ Handle const *other() const;
+
+ static char const *handle_type_to_localized_string(NodeType type);
+
+protected:
+
+ Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent);
+ virtual void handle_2button_press();
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ Glib::ustring _getTip(unsigned state) const override;
+ Glib::ustring _getDragTip(GdkEventMotion *event) const override;
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ inline PathManipulator &_pm();
+ inline PathManipulator &_pm() const;
+ Node *_parent; // the handle's lifetime does not extend beyond that of the parent node,
+ // so a naked pointer is OK and allows setting it during Node's construction
+ SPCtrlLine *_handle_line;
+ bool _degenerate; // True if the handle is retracted, i.e. has zero length. This is used often internally so it makes sense to cache this
+
+ /**
+ * Control point of a cubic Bezier curve in a path.
+ *
+ * Handle keeps the node type invariant only for the opposite handle of the same node.
+ * Keeping the invariant on node moves is left to the %Node class.
+ */
+ static Geom::Point _saved_other_pos;
+
+ static double _saved_length;
+ static bool _drag_out;
+ static ColorSet _handle_colors;
+ friend class Node;
+};
+
+class Node : ListNode, public SelectableControlPoint {
+public:
+
+ /**
+ * Curve endpoint in an editable path.
+ *
+ * The method move() keeps node type invariants during translations.
+ */
+ Node(NodeSharedData const &data, Geom::Point const &pos);
+
+ Node(Node const &) = delete;
+
+ void move(Geom::Point const &p) override;
+ void transform(Geom::Affine const &m) override;
+ Geom::Rect bounds() const override;
+
+ NodeType type() const { return _type; }
+
+ /**
+ * Sets the node type and optionally restores the invariants associated with the given type.
+ * @param type The type to set.
+ * @param update_handles Whether to restore invariants associated with the given type.
+ * Passing false is useful e.g. when initially creating the path,
+ * and when making cusp nodes during some node algorithms.
+ * Pass true when used in response to an UI node type button.
+ */
+ void setType(NodeType type, bool update_handles = true);
+
+ void showHandles(bool v);
+
+ void updateHandles();
+
+
+ /**
+ * Pick the best type for this node, based on the position of its handles.
+ * This is what assigns types to nodes created using the pen tool.
+ */
+ void pickBestType(); // automatically determine the type from handle positions
+
+ bool isDegenerate() const { return _front.isDegenerate() && _back.isDegenerate(); }
+ bool isEndNode() const;
+ Handle *front() { return &_front; }
+ Handle *back() { return &_back; }
+
+ /**
+ * Gets the handle that faces the given adjacent node.
+ * Will abort with error if the given node is not adjacent.
+ */
+ Handle *handleToward(Node *to);
+
+ /**
+ * Gets the node in the direction of the given handle.
+ * Will abort with error if the handle doesn't belong to this node.
+ */
+ Node *nodeToward(Handle *h);
+
+ /**
+ * Gets the handle that goes in the direction opposite to the given adjacent node.
+ * Will abort with error if the given node is not adjacent.
+ */
+ Handle *handleAwayFrom(Node *to);
+
+ /**
+ * Gets the node in the direction opposite to the given handle.
+ * Will abort with error if the handle doesn't belong to this node.
+ */
+ Node *nodeAwayFrom(Handle *h);
+
+ NodeList &nodeList() { return *(static_cast<ListNode*>(this)->ln_list); }
+ NodeList &nodeList() const { return *(static_cast<ListNode const*>(this)->ln_list); }
+
+ /**
+ * Move the node to the bottom of its canvas group.
+ * Useful for node break, to ensure that the selected nodes are above the unselected ones.
+ */
+ void sink();
+
+ static NodeType parse_nodetype(char x);
+ static char const *node_type_to_localized_string(NodeType type);
+
+ // temporarily public
+ /** Customized event handler to catch scroll events needed for selection grow/shrink. */
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override;
+
+ Inkscape::SnapCandidatePoint snapCandidatePoint();
+
+protected:
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ void _setState(State state) override;
+ Glib::ustring _getTip(unsigned state) const override;
+ Glib::ustring _getDragTip(GdkEventMotion *event) const override;
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ void _fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos);
+ void _updateAutoHandles();
+
+ /**
+ * Select or deselect a node in this node's subpath based on its path distance from this node.
+ * @param dir If negative, shrink selection by one node; if positive, grow by one node.
+ */
+ void _linearGrow(int dir);
+
+ Node *_next();
+ Node const *_next() const;
+ Node *_prev();
+ Node const *_prev() const;
+ Inkscape::SnapSourceType _snapSourceType() const;
+ Inkscape::SnapTargetType _snapTargetType() const;
+ inline PathManipulator &_pm();
+ inline PathManipulator &_pm() const;
+
+ /** Determine whether two nodes are joined by a linear segment. */
+ static bool _is_line_segment(Node *first, Node *second);
+
+ // Handles are always present, but are not visible if they coincide with the node
+ // (are degenerate). A segment that has both handles degenerate is always treated
+ // as a line segment
+ Handle _front; ///< Node handle in the backward direction of the path
+ Handle _back; ///< Node handle in the forward direction of the path
+ NodeType _type; ///< Type of node - cusp, smooth...
+ bool _handles_shown;
+ static ColorSet node_colors;
+
+ friend class Handle;
+ friend class NodeList;
+ friend class NodeIterator<Node>;
+ friend class NodeIterator<Node const>;
+};
+
+/// Iterator for editable nodes
+/** Use this class for all operations that require some knowledge about the node's
+ * neighbors. It is a bidirectional iterator.
+ *
+ * Because paths can be cyclic, node iterators have two different ways to
+ * increment and decrement them. When using ++/--, the end iterator will eventually
+ * be returned. When using advance()/retreat(), the end iterator will only be returned
+ * when the path is open. If it's closed, calling advance() will cycle indefinitely.
+ * This is particularly useful for cases where the adjacency of nodes is more important
+ * than their sequence order.
+ *
+ * When @a i is a node iterator, then:
+ * - <code>++i</code> moves the iterator to the next node in sequence order;
+ * - <code>--i</code> moves the iterator to the previous node in sequence order;
+ * - <code>i.next()</code> returns the next node with wrap-around;
+ * - <code>i.prev()</code> returns the previous node with wrap-around;
+ * - <code>i.advance()</code> moves the iterator to the next node with wrap-around;
+ * - <code>i.retreat()</code> moves the iterator to the previous node with wrap-around.
+ *
+ * next() and prev() do not change their iterator. They can return the end iterator
+ * if the path is open.
+ *
+ * Unlike most other iterators, you can check whether you've reached the end of the list
+ * without having access to the iterator's container.
+ * Simply use <code>if (i) { ...</code>
+ * */
+template <typename N>
+class NodeIterator
+ : public boost::bidirectional_iterator_helper<NodeIterator<N>, N, std::ptrdiff_t,
+ N *, N &>
+{
+public:
+ typedef NodeIterator self;
+ NodeIterator()
+ : _node(nullptr)
+ {}
+ // default copy, default assign
+
+ self &operator++() {
+ _node = (_node?_node->ln_next:nullptr);
+ return *this;
+ }
+ self &operator--() {
+ _node = (_node?_node->ln_prev:nullptr);
+ return *this;
+ }
+ bool operator==(self const &other) const { return _node == other._node; }
+ N &operator*() const { return *static_cast<N*>(_node); }
+ inline operator bool() const; // define after NodeList
+ /// Get a pointer to the underlying node. Equivalent to <code>&*i</code>.
+ N *get_pointer() const { return static_cast<N*>(_node); }
+ /// @see get_pointer()
+ N *ptr() const { return static_cast<N*>(_node); }
+
+ self next() const {
+ self r(*this);
+ r.advance();
+ return r;
+ }
+ self prev() const {
+ self r(*this);
+ r.retreat();
+ return r;
+ }
+ self &advance();
+ self &retreat();
+private:
+ NodeIterator(ListNode const *n)
+ : _node(const_cast<ListNode*>(n))
+ {}
+ ListNode *_node;
+ friend class NodeList;
+};
+
+class NodeList : ListNode, boost::noncopyable {
+public:
+ typedef std::size_t size_type;
+ typedef Node &reference;
+ typedef Node const &const_reference;
+ typedef Node *pointer;
+ typedef Node const *const_pointer;
+ typedef Node value_type;
+ typedef NodeIterator<value_type> iterator;
+ typedef NodeIterator<value_type const> const_iterator;
+
+ // TODO Lame. Make this private and make SubpathList a factory
+ /**
+ * An editable list of nodes representing a subpath.
+ *
+ * It can optionally be cyclic to represent a closed path.
+ * The list has iterators that act like plain node iterators, but can also be used
+ * to obtain shared pointers to nodes.
+ */
+ NodeList(SubpathList &_list);
+
+ ~NodeList();
+
+ // no copy or assign
+ NodeList(NodeList const &) = delete;
+ void operator=(NodeList const &) = delete;
+
+ // iterators
+ iterator begin() { return iterator(ln_next); }
+ iterator end() { return iterator(this); }
+ const_iterator begin() const { return const_iterator(ln_next); }
+ const_iterator end() const { return const_iterator(this); }
+
+ // size
+ bool empty();
+ size_type size();
+
+ // extra node-specific methods
+ bool closed();
+
+ /**
+ * A subpath is degenerate if it has no segments - either one node in an open path
+ * or no nodes in a closed path.
+ */
+ bool degenerate();
+
+ void setClosed(bool c) { _closed = c; }
+ iterator before(double t, double *fracpart = nullptr);
+ iterator before(Geom::PathTime const &pvp);
+ const_iterator before(double t, double *fracpart = nullptr) const {
+ return const_cast<NodeList *>(this)->before(t, fracpart)._node;
+ }
+ const_iterator before(Geom::PathTime const &pvp) const {
+ return const_cast<NodeList *>(this)->before(pvp)._node;
+ }
+
+ // list operations
+
+ /** insert a node before pos. */
+ iterator insert(iterator pos, Node *x);
+
+ template <class InputIterator>
+ void insert(iterator pos, InputIterator first, InputIterator last) {
+ for (; first != last; ++first) insert(pos, *first);
+ }
+ void splice(iterator pos, NodeList &list);
+ void splice(iterator pos, NodeList &list, iterator i);
+ void splice(iterator pos, NodeList &list, iterator first, iterator last);
+ void reverse();
+ void shift(int n);
+ void push_front(Node *x) { insert(begin(), x); }
+ void pop_front() { erase(begin()); }
+ void push_back(Node *x) { insert(end(), x); }
+ void pop_back() { erase(--end()); }
+ void clear();
+ iterator erase(iterator pos);
+ iterator erase(iterator first, iterator last) {
+ NodeList::iterator ret = first;
+ while (first != last) ret = erase(first++);
+ return ret;
+ }
+
+ // member access - undefined results when the list is empty
+ Node &front() { return *static_cast<Node*>(ln_next); }
+ Node &back() { return *static_cast<Node*>(ln_prev); }
+
+ // HACK remove this subpath from its path. This will be removed later.
+ void kill();
+ SubpathList &subpathList() { return _list; }
+
+ static iterator get_iterator(Node *n) { return iterator(n); }
+ static const_iterator get_iterator(Node const *n) { return const_iterator(n); }
+ static NodeList &get(Node *n);
+ static NodeList &get(iterator const &i);
+private:
+
+ SubpathList &_list;
+ bool _closed;
+
+ friend class Node;
+ friend class Handle; // required to access handle and handle line groups
+ friend class NodeIterator<Node>;
+ friend class NodeIterator<Node const>;
+};
+
+/**
+ * List of node lists. Represents an editable path.
+ * Editable path composed of one or more subpaths.
+ */
+class SubpathList : public std::list< std::shared_ptr<NodeList> > {
+public:
+ typedef std::list< std::shared_ptr<NodeList> > list_type;
+
+ SubpathList(PathManipulator &pm) : _path_manipulator(pm) {}
+ PathManipulator &pm() { return _path_manipulator; }
+
+private:
+ list_type _nodelists;
+ PathManipulator &_path_manipulator;
+ friend class NodeList;
+ friend class Node;
+ friend class Handle;
+};
+
+
+
+// define inline Handle funcs after definition of Node
+inline Geom::Point Handle::relativePos() const {
+ return position() - _parent->position();
+}
+inline void Handle::setRelativePos(Geom::Point const &p) {
+ setPosition(_parent->position() + p);
+}
+inline double Handle::length() const {
+ return relativePos().length();
+}
+inline PathManipulator &Handle::_pm() {
+ return _parent->_pm();
+}
+inline PathManipulator &Handle::_pm() const {
+ return _parent->_pm();
+}
+inline PathManipulator &Node::_pm() {
+ return nodeList().subpathList().pm();
+}
+
+inline PathManipulator &Node::_pm() const {
+ return nodeList().subpathList().pm();
+}
+
+// definitions for node iterator
+template <typename N>
+NodeIterator<N>::operator bool() const {
+ return _node && static_cast<ListNode*>(_node->ln_list) != _node;
+}
+template <typename N>
+NodeIterator<N> &NodeIterator<N>::advance() {
+ ++(*this);
+ if (G_UNLIKELY(!*this) && _node->ln_list->closed()) ++(*this);
+ return *this;
+}
+template <typename N>
+NodeIterator<N> &NodeIterator<N>::retreat() {
+ --(*this);
+ if (G_UNLIKELY(!*this) && _node->ln_list->closed()) --(*this);
+ return *this;
+}
+
+} // 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/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp
new file mode 100644
index 0000000..e5b319b
--- /dev/null
+++ b/src/ui/tool/path-manipulator.cpp
@@ -0,0 +1,1756 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Path manipulator - implementation.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/bezier-utils.h>
+#include <2geom/path-sink.h>
+
+#include <utility>
+
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-util.h"
+#include "display/curve.h"
+#include "display/canvas-bpath.h"
+
+#include "helper/geom.h"
+
+#include "live_effects/lpeobject.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpe-powerstroke.h"
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/parameter/path.h"
+
+#include "object/sp-path.h"
+#include "style.h"
+
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/curve-drag-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+/// Types of path changes that we must react to.
+enum PathChange {
+ PATH_CHANGE_D,
+ PATH_CHANGE_TRANSFORM
+};
+
+} // anonymous namespace
+const double HANDLE_CUBIC_GAP = 0.001;
+const double NO_POWER = 0.0;
+const double DEFAULT_START_POWER = 1.0/3.0;
+
+
+/**
+ * Notifies the path manipulator when something changes the path being edited
+ * (e.g. undo / redo)
+ */
+class PathManipulatorObserver : public Inkscape::XML::NodeObserver {
+public:
+ PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node)
+ : _pm(p)
+ , _node(node)
+ , _blocked(false)
+ {
+ Inkscape::GC::anchor(_node);
+ _node->addObserver(*this);
+ }
+
+ ~PathManipulatorObserver() override {
+ _node->removeObserver(*this);
+ Inkscape::GC::release(_node);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr,
+ Util::ptr_shared, Util::ptr_shared) override
+ {
+ // do nothing if blocked
+ if (_blocked) return;
+
+ GQuark path_d = g_quark_from_static_string("d");
+ GQuark path_transform = g_quark_from_static_string("transform");
+ GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data());
+
+ // only react to "d" (path data) and "transform" attribute changes
+ if (attr == lpe_quark || attr == path_d) {
+ _pm->_externalChange(PATH_CHANGE_D);
+ } else if (attr == path_transform) {
+ _pm->_externalChange(PATH_CHANGE_TRANSFORM);
+ }
+ }
+
+ void block() { _blocked = true; }
+ void unblock() { _blocked = false; }
+private:
+ PathManipulator *_pm;
+ Inkscape::XML::Node *_node;
+ bool _blocked;
+};
+
+void build_segment(Geom::PathBuilder &, Node *, Node *);
+PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPObject *path,
+ Geom::Affine const &et, guint32 outline_color, Glib::ustring lpe_key)
+ : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection)
+ , _subpaths(*this)
+ , _multi_path_manipulator(mpm)
+ , _path(path)
+ , _spcurve(new SPCurve())
+ , _dragpoint(new CurveDragPoint(*this))
+ , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr()))
+ , _edit_transform(et)
+ , _show_handles(true)
+ , _show_outline(false)
+ , _show_path_direction(false)
+ , _live_outline(true)
+ , _live_objects(true)
+ , _is_bspline(false)
+ , _lpe_key(std::move(lpe_key))
+{
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *pathshadow = dynamic_cast<SPPath *>(_path);
+ if (!lpeobj) {
+ _i2d_transform = pathshadow->i2dt_affine();
+ } else {
+ _i2d_transform = Geom::identity();
+ }
+ _d2i_transform = _i2d_transform.inverse();
+ _dragpoint->setVisible(false);
+
+ _getGeometry();
+
+ _outline = sp_canvas_bpath_new(_multi_path_manipulator._path_data.outline_group, nullptr);
+ sp_canvas_item_hide(_outline);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(_outline), outline_color, 1.0,
+ SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(_outline), 0, SP_WIND_RULE_NONZERO);
+
+ _selection.signal_update.connect(
+ sigc::bind(sigc::mem_fun(*this, &PathManipulator::update), false));
+ _selection.signal_selection_changed.connect(
+ sigc::mem_fun(*this, &PathManipulator::_selectionChangedM));
+ _desktop->signal_zoom_changed.connect(
+ sigc::hide( sigc::mem_fun(*this, &PathManipulator::_updateOutlineOnZoomChange)));
+
+ _createControlPointsFromGeometry();
+ //Define if the path is BSpline on construction
+ _recalculateIsBSpline();
+}
+
+PathManipulator::~PathManipulator()
+{
+ delete _dragpoint;
+ delete _observer;
+ sp_canvas_item_destroy(_outline);
+ _spcurve->unref();
+ clear();
+}
+
+/** Handle motion events to update the position of the curve drag point. */
+bool PathManipulator::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event)
+{
+ if (empty()) return false;
+
+ switch (event->type)
+ {
+ case GDK_MOTION_NOTIFY:
+ _updateDragPoint(event_point(event->motion));
+ break;
+ default:
+ break;
+ }
+ return false;
+}
+
+/** Check whether the manipulator has any nodes. */
+bool PathManipulator::empty() {
+ return !_path || _subpaths.empty();
+}
+
+/** Update the display and the outline of the path.
+ * \param alert_LPE if true, alerts an applied LPE to what the path is going to be changed to, so it can adjust its parameters for nicer user interfacing
+ */
+void PathManipulator::update(bool alert_LPE)
+{
+ _createGeometryFromControlPoints(alert_LPE);
+}
+
+/** Store the changes to the path in XML. */
+void PathManipulator::writeXML()
+{
+ if (!_live_outline)
+ _updateOutline();
+
+ _setGeometry();
+
+ if (_path) {
+ _observer->block();
+ if (!empty()) {
+ _path->updateRepr();
+ _getXMLNode()->setAttribute(_nodetypesKey(), _createTypeString());
+ }
+ else {
+ // this manipulator will have to be destroyed right after this call
+ _getXMLNode()->removeObserver(*_observer);
+ _path->deleteObject(true, true);
+ _path = nullptr;
+ }
+ _observer->unblock();
+ }
+}
+
+/** Remove all nodes from the path. */
+void PathManipulator::clear()
+{
+ // no longer necessary since nodes remove themselves from selection on destruction
+ //_removeNodesFromSelection();
+ _subpaths.clear();
+}
+
+/** Select all nodes in subpaths that have something selected. */
+void PathManipulator::selectSubpaths()
+{
+ for (auto & _subpath : _subpaths) {
+ NodeList::iterator sp_start = _subpath->begin(), sp_end = _subpath->end();
+ for (NodeList::iterator j = sp_start; j != sp_end; ++j) {
+ if (j->selected()) {
+ // if at least one of the nodes from this subpath is selected,
+ // select all nodes from this subpath
+ for (NodeList::iterator ins = sp_start; ins != sp_end; ++ins)
+ _selection.insert(ins.ptr());
+ continue;
+ }
+ }
+ }
+}
+
+/** Invert selection in the selected subpaths. */
+void PathManipulator::invertSelectionInSubpaths()
+{
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ // found selected node - invert selection in this subpath
+ for (NodeList::iterator k = _subpath->begin(); k != _subpath->end(); ++k) {
+ if (k->selected()) _selection.erase(k.ptr());
+ else _selection.insert(k.ptr());
+ }
+ // next subpath
+ break;
+ }
+ }
+ }
+}
+
+/** Insert a new node in the middle of each selected segment. */
+void PathManipulator::insertNodes()
+{
+ if (_selection.size() < 2) return;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ NodeList::iterator k = j.next();
+ if (k && j->selected() && k->selected()) {
+ j = subdivideSegment(j, 0.5);
+ _selection.insert(j.ptr());
+ }
+ }
+ }
+}
+
+void PathManipulator::insertNode(Geom::Point pt)
+{
+ Geom::Coord dist = _updateDragPoint(pt);
+ if (dist < 1e-5) { // 1e-6 is too small, as observed occasionally when inserting a node at a snapped intersection of paths
+ insertNode(_dragpoint->getIterator(), _dragpoint->getTimeValue(), true);
+ }
+}
+
+void PathManipulator::insertNode(NodeList::iterator first, double t, bool take_selection)
+{
+ NodeList::iterator inserted = subdivideSegment(first, t);
+ if (take_selection) {
+ _selection.clear();
+ }
+ _selection.insert(inserted.ptr());
+
+ update(true);
+ _commit(_("Add node"));
+}
+
+
+static void
+add_or_replace_if_extremum(std::vector< std::pair<NodeList::iterator, double> > &vec,
+ double & extrvalue, double testvalue, NodeList::iterator const& node, double t)
+{
+ if (testvalue > extrvalue) {
+ // replace all extreme nodes with the new one
+ vec.clear();
+ vec.emplace_back( node, t );
+ extrvalue = testvalue;
+ } else if ( Geom::are_near(testvalue, extrvalue) ) {
+ // very rare but: extremum node at the same extreme value!!! so add it to the list
+ vec.emplace_back( node, t );
+ }
+}
+
+/** Insert a new node at the extremum of the selected segments. */
+void PathManipulator::insertNodeAtExtremum(ExtremumType extremum)
+{
+ if (_selection.size() < 2) return;
+
+ double sign = (extremum == EXTR_MIN_X || extremum == EXTR_MIN_Y) ? -1. : 1.;
+ Geom::Dim2 dim = (extremum == EXTR_MIN_X || extremum == EXTR_MAX_X) ? Geom::X : Geom::Y;
+
+ for (auto & _subpath : _subpaths) {
+ Geom::Coord extrvalue = - Geom::infinity();
+ std::vector< std::pair<NodeList::iterator, double> > extremum_vector;
+
+ for (NodeList::iterator first = _subpath->begin(); first != _subpath->end(); ++first) {
+ NodeList::iterator second = first.next();
+ if (second && first->selected() && second->selected()) {
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * first->position()[dim], first, 0.);
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * second->position()[dim], first, 1.);
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ // a line segment has is extrema at the start and end, no node should be added
+ continue;
+ } else {
+ // build 1D cubic bezier curve
+ Geom::Bezier temp1d(first->position()[dim], first->front()->position()[dim],
+ second->back()->position()[dim], second->position()[dim]);
+ // and determine extremum
+ Geom::Bezier deriv1d = derivative(temp1d);
+ std::vector<double> rs = deriv1d.roots();
+ for (double & r : rs) {
+ add_or_replace_if_extremum(extremum_vector, extrvalue, sign * temp1d.valueAt(r), first, r);
+ }
+ }
+ }
+ }
+
+ for (auto & i : extremum_vector) {
+ // don't insert node at the start or end of a segment, i.e. round values for extr_t
+ double t = i.second;
+ if ( !Geom::are_near(t - std::floor(t+0.5),0.) ) // std::floor(t+0.5) is another way of writing round(t)
+ {
+ _selection.insert( subdivideSegment(i.first, t).ptr() );
+ }
+ }
+ }
+}
+
+
+/** Insert new nodes exactly at the positions of selected nodes while preserving shape.
+ * This is equivalent to breaking, except that it doesn't split into subpaths. */
+void PathManipulator::duplicateNodes()
+{
+ if (_selection.empty()) return;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ NodeList::iterator k = j.next();
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, *j);
+
+ if (k) {
+ // Move the new node to the bottom of the Z-order. This way you can drag all
+ // nodes that were selected before this operation without deselecting
+ // everything because there is a new node above.
+ n->sink();
+ }
+
+ n->front()->setPosition(*j->front());
+ j->front()->retract();
+ j->setType(NODE_CUSP, false);
+ _subpath->insert(k, n);
+
+ if (k) {
+ // We need to manually call the selection change callback to refresh
+ // the handle display correctly.
+ // This call changes num_selected, but we call this once for a selected node
+ // and once for an unselected node, so in the end the number stays correct.
+ _selectionChanged(j.ptr(), true);
+ _selectionChanged(n, false);
+ } else {
+ // select the new end node instead of the node just before it
+ _selection.erase(j.ptr());
+ _selection.insert(n);
+ break; // this was the end node, nothing more to do
+ }
+ }
+ }
+ }
+}
+
+/** Replace contiguous selections of nodes in each subpath with one node. */
+void PathManipulator::weldNodes(NodeList::iterator preserve_pos)
+{
+ if (_selection.size() < 2) return;
+ hideDragPoint();
+
+ bool pos_valid = preserve_pos;
+ for (auto sp : _subpaths) {
+ unsigned num_selected = 0, num_unselected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+ if (num_selected < 2) continue;
+ if (num_unselected == 0) {
+ // if all nodes in a subpath are selected, the operation doesn't make much sense
+ continue;
+ }
+
+ // Start from unselected node in closed paths, so that we don't start in the middle
+ // of a selection
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+
+ // Work loop
+ while (num_selected > 0) {
+ // Find selected node
+ while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next();
+ if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, "
+ "but there are still nodes to process!");
+
+ // note: this is initialized to zero, because the loop below counts sel_beg as well
+ // the loop conditions are simpler that way
+ unsigned num_points = 0;
+ bool use_pos = false;
+ Geom::Point back_pos, front_pos;
+ back_pos = *sel_beg->back();
+
+ for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) {
+ ++num_points;
+ front_pos = *sel_end->front();
+ if (pos_valid && sel_end == preserve_pos) use_pos = true;
+ }
+ if (num_points > 1) {
+ Geom::Point joined_pos;
+ if (use_pos) {
+ joined_pos = preserve_pos->position();
+ pos_valid = false;
+ } else {
+ joined_pos = Geom::middle_point(back_pos, front_pos);
+ }
+ sel_beg->setType(NODE_CUSP, false);
+ sel_beg->move(joined_pos);
+ // do not move handles if they aren't degenerate
+ if (!sel_beg->back()->isDegenerate()) {
+ sel_beg->back()->setPosition(back_pos);
+ }
+ if (!sel_end.prev()->front()->isDegenerate()) {
+ sel_beg->front()->setPosition(front_pos);
+ }
+ sel_beg = sel_beg.next();
+ while (sel_beg != sel_end) {
+ NodeList::iterator next = sel_beg.next();
+ sp->erase(sel_beg);
+ sel_beg = next;
+ --num_selected;
+ }
+ }
+ --num_selected; // for the joined node or single selected node
+ }
+ }
+}
+
+/** Remove nodes in the middle of selected segments. */
+void PathManipulator::weldSegments()
+{
+ if (_selection.size() < 2) return;
+ hideDragPoint();
+
+ for (auto sp : _subpaths) {
+ unsigned num_selected = 0, num_unselected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+
+ // if 2 or fewer nodes are selected, there can't be any middle points to remove.
+ if (num_selected <= 2) continue;
+
+ if (num_unselected == 0 && sp->closed()) {
+ // if all nodes in a closed subpath are selected, the operation doesn't make much sense
+ continue;
+ }
+
+ // Start from unselected node in closed paths, so that we don't start in the middle
+ // of a selection
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+
+ // Work loop
+ while (num_selected > 0) {
+ // Find selected node
+ while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next();
+ if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, "
+ "but there are still nodes to process!");
+
+ // note: this is initialized to zero, because the loop below counts sel_beg as well
+ // the loop conditions are simpler that way
+ unsigned num_points = 0;
+
+ // find the end of selected segment
+ for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) {
+ ++num_points;
+ }
+ if (num_points > 2) {
+ // remove nodes in the middle
+ // TODO: fit bezier to the former shape
+ sel_beg = sel_beg.next();
+ while (sel_beg != sel_end.prev()) {
+ NodeList::iterator next = sel_beg.next();
+ sp->erase(sel_beg);
+ sel_beg = next;
+ }
+ }
+ sel_beg = sel_end;
+ // decrease num_selected by the number of points processed
+ num_selected -= num_points;
+ }
+ }
+}
+
+/** Break the subpath at selected nodes. It also works for single node closed paths. */
+void PathManipulator::breakNodes()
+{
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) {
+ SubpathPtr sp = *i;
+ NodeList::iterator cur = sp->begin(), end = sp->end();
+ if (!sp->closed()) {
+ // Each open path must have at least two nodes so no checks are required.
+ // For 2-node open paths, cur == end
+ ++cur;
+ --end;
+ }
+ for (; cur != end; ++cur) {
+ if (!cur->selected()) continue;
+ SubpathPtr ins;
+ bool becomes_open = false;
+
+ if (sp->closed()) {
+ // Move the node to break at to the beginning of path
+ if (cur != sp->begin())
+ sp->splice(sp->begin(), *sp, cur, sp->end());
+ sp->setClosed(false);
+ ins = sp;
+ becomes_open = true;
+ } else {
+ SubpathPtr new_sp(new NodeList(_subpaths));
+ new_sp->splice(new_sp->end(), *sp, sp->begin(), cur);
+ _subpaths.insert(i, new_sp);
+ ins = new_sp;
+ }
+
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, cur->position());
+ ins->insert(ins->end(), n);
+ cur->setType(NODE_CUSP, false);
+ n->back()->setRelativePos(cur->back()->relativePos());
+ cur->back()->retract();
+ n->sink();
+
+ if (becomes_open) {
+ cur = sp->begin(); // this will be increased to ++sp->begin()
+ end = --sp->end();
+ }
+ }
+ }
+}
+
+/** Delete selected nodes in the path, optionally substituting deleted segments with bezier curves
+ * in a way that attempts to preserve the original shape of the curve. */
+void PathManipulator::deleteNodes(bool keep_shape)
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+
+ // If there are less than 2 unselected nodes in an open subpath or no unselected nodes
+ // in a closed one, delete entire subpath.
+ unsigned num_unselected = 0, num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+ if (num_selected == 0) {
+ ++i;
+ continue;
+ }
+ if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ // In closed paths, start from an unselected node - otherwise we might start in the middle
+ // of a selected stretch and the resulting bezier fit would be suboptimal
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+ sel_end = sel_beg;
+
+ while (num_selected > 0) {
+ while (sel_beg && !sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ }
+ sel_end = sel_beg;
+
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ }
+
+ num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape);
+ sel_beg = sel_end;
+ }
+ ++i;
+ }
+}
+
+/**
+ * Delete nodes between the two iterators.
+ * The given range can cross the beginning of the subpath in closed subpaths.
+ * @param start Beginning of the range to delete
+ * @param end End of the range
+ * @param keep_shape Whether to fit the handles at surrounding nodes to approximate
+ * the shape before deletion
+ * @return Number of deleted nodes
+ */
+unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, bool keep_shape)
+{
+ unsigned const samples_per_segment = 10;
+ double const t_step = 1.0 / samples_per_segment;
+
+ unsigned del_len = 0;
+ for (NodeList::iterator i = start; i != end; i = i.next()) {
+ ++del_len;
+ }
+ if (del_len == 0) return 0;
+
+ // set surrounding node types to cusp if:
+ // 1. keep_shape is on, or
+ // 2. we are deleting at the end or beginning of an open path
+ if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false);
+ if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false);
+
+ if (keep_shape && start.prev() && end) {
+ unsigned num_samples = (del_len + 1) * samples_per_segment + 1;
+ Geom::Point *bezier_data = new Geom::Point[num_samples];
+ Geom::Point result[4];
+ unsigned seg = 0;
+
+ for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) {
+ Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back());
+ for (unsigned s = 0; s < samples_per_segment; ++s) {
+ bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s);
+ }
+ ++seg;
+ }
+ // Fill last point
+ bezier_data[num_samples - 1] = end->position();
+ // Compute replacement bezier curve
+ // TODO the fitting algorithm sucks - rewrite it to be awesome
+ bezier_fit_cubic(result, bezier_data, num_samples, 0.5);
+ delete[] bezier_data;
+
+ start.prev()->front()->setPosition(result[1]);
+ end->back()->setPosition(result[2]);
+ }
+
+ // We can't use nl->erase(start, end), because it would break when the stretch
+ // crosses the beginning of a closed subpath
+ NodeList &nl = start->nodeList();
+ while (start != end) {
+ NodeList::iterator next = start.next();
+ nl.erase(start);
+ start = next;
+ }
+ // if we are removing, we readjust the handlers
+ if(_isBSpline()){
+ if(start.prev()){
+ double bspline_weight = _bsplineHandlePosition(start.prev()->back(), false);
+ start.prev()->front()->setPosition(_bsplineHandleReposition(start.prev()->front(), bspline_weight));
+ }
+ if(end){
+ double bspline_weight = _bsplineHandlePosition(end->front(), false);
+ end->back()->setPosition(_bsplineHandleReposition(end->back(),bspline_weight));
+ }
+ }
+
+ return del_len;
+}
+
+/** Removes selected segments */
+void PathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+ bool has_unselected = false;
+ unsigned num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) {
+ ++num_selected;
+ } else {
+ has_unselected = true;
+ }
+ }
+ if (!has_unselected) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ NodeList::iterator sel_beg = sp->begin();
+ if (sp->closed()) {
+ while (sel_beg && sel_beg->selected()) ++sel_beg;
+ }
+ while (num_selected > 0) {
+ if (!sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ continue;
+ }
+ NodeList::iterator sel_end = sel_beg;
+ unsigned num_points = 0;
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ ++num_points;
+ }
+ if (num_points >= 2) {
+ // Retract end handles
+ sel_end.prev()->setType(NODE_CUSP, false);
+ sel_end.prev()->back()->retract();
+ sel_beg->setType(NODE_CUSP, false);
+ sel_beg->front()->retract();
+ if (sp->closed()) {
+ // In closed paths, relocate the beginning of the path to the last selected
+ // node and then unclose it. Remove the nodes from the first selected node
+ // to the new end of path.
+ if (sel_end.prev() != sp->begin())
+ sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end());
+ sp->setClosed(false);
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ // for open paths:
+ // 1. At end or beginning, delete including the node on the end or beginning
+ // 2. In the middle, delete only inner nodes
+ if (sel_beg == sp->begin()) {
+ sp->erase(sp->begin(), sel_end.prev());
+ } else if (sel_end == sp->end()) {
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ SubpathPtr new_sp(new NodeList(_subpaths));
+ new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next());
+ _subpaths.insert(i, new_sp);
+ if (sel_end.prev())
+ sp->erase(sp->begin(), sel_end.prev());
+ }
+ }
+ }
+ sel_beg = sel_end;
+ num_selected -= num_points;
+ }
+ ++i;
+ }
+}
+
+/** Reverse subpaths of the path.
+ * @param selected_only If true, only paths that have at least one selected node
+ * will be reversed. Otherwise all subpaths will be reversed. */
+void PathManipulator::reverseSubpaths(bool selected_only)
+{
+ for (auto & _subpath : _subpaths) {
+ if (selected_only) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ _subpath->reverse();
+ break; // continue with the next subpath
+ }
+ }
+ } else {
+ _subpath->reverse();
+ }
+ }
+}
+
+/** Make selected segments curves / lines. */
+void PathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ NodeList::iterator k = j.next();
+ if (!(k && j->selected() && k->selected())) continue;
+ switch (type) {
+ case SEGMENT_STRAIGHT:
+ if (j->front()->isDegenerate() && k->back()->isDegenerate())
+ break;
+ j->front()->move(*j);
+ k->back()->move(*k);
+ break;
+ case SEGMENT_CUBIC_BEZIER:
+ if (!j->front()->isDegenerate() || !k->back()->isDegenerate())
+ break;
+ // move both handles to 1/3 of the line
+ j->front()->move(j->position() + (k->position() - j->position()) / 3);
+ k->back()->move(k->position() + (j->position() - k->position()) / 3);
+ break;
+ }
+ }
+ }
+}
+
+void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) {
+ n->setType(NODE_SMOOTH);
+ }
+ Handle *h = _chooseHandle(n, which);
+ double length_change;
+
+ if (pixel) {
+ length_change = 1.0 / _desktop->current_zoom() * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
+ length_change *= dir;
+ }
+
+ Geom::Point relpos;
+ if (h->isDegenerate()) {
+ if (dir < 0) return;
+ Node *nh = n->nodeToward(h);
+ if (!nh) return;
+ relpos = Geom::unit_vector(nh->position() - n->position()) * length_change;
+ } else {
+ relpos = h->relativePos();
+ double rellen = relpos.length();
+ relpos *= ((rellen + length_change) / rellen);
+ }
+ h->setRelativePos(relpos);
+ update();
+ gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right";
+ _commit(_("Scale handle"), key);
+}
+
+void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() != NODE_CUSP) {
+ n->setType(NODE_CUSP);
+ }
+ Handle *h = _chooseHandle(n, which);
+ if (h->isDegenerate()) return;
+
+ double angle;
+ if (pixel) {
+ // Rotate by "one pixel"
+ angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ angle = M_PI * dir / snaps;
+ }
+
+ h->setRelativePos(h->relativePos() * Geom::Rotate(angle));
+ update();
+ gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right";
+ _commit(_("Rotate handle"), key);
+}
+
+Handle *PathManipulator::_chooseHandle(Node *n, int which)
+{
+ NodeList::iterator i = NodeList::get_iterator(n);
+ Node *prev = i.prev().ptr();
+ Node *next = i.next().ptr();
+
+ // on an endnode, the remaining handle automatically wins
+ if (!next) return n->back();
+ if (!prev) return n->front();
+
+ // compare X coord offline segments
+ Geom::Point npos = next->position();
+ Geom::Point ppos = prev->position();
+ if (which < 0) {
+ // pick left handle.
+ // we just swap the handles and pick the right handle below.
+ std::swap(npos, ppos);
+ }
+
+ if (npos[Geom::X] >= ppos[Geom::X]) {
+ return n->front();
+ } else {
+ return n->back();
+ }
+}
+
+/** Set the visibility of handles. */
+void PathManipulator::showHandles(bool show)
+{
+ if (show == _show_handles) return;
+ if (show) {
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (!j->selected()) continue;
+ j->showHandles(true);
+ if (j.prev()) j.prev()->showHandles(true);
+ if (j.next()) j.next()->showHandles(true);
+ }
+ }
+ } else {
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.showHandles(false);
+ }
+ }
+ }
+ _show_handles = show;
+}
+
+/** Set the visibility of outline. */
+void PathManipulator::showOutline(bool show)
+{
+ if (show == _show_outline) return;
+ _show_outline = show;
+ _updateOutline();
+}
+
+void PathManipulator::showPathDirection(bool show)
+{
+ if (show == _show_path_direction) return;
+ _show_path_direction = show;
+ _updateOutline();
+}
+
+void PathManipulator::setLiveOutline(bool set)
+{
+ _live_outline = set;
+}
+
+void PathManipulator::setLiveObjects(bool set)
+{
+ _live_objects = set;
+}
+
+void PathManipulator::updateHandles()
+{
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.updateHandles();
+ }
+ }
+}
+
+void PathManipulator::setControlsTransform(Geom::Affine const &tnew)
+{
+ Geom::Affine delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform;
+ _edit_transform = tnew;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(delta);
+ }
+ }
+ _createGeometryFromControlPoints();
+}
+
+/** Hide the curve drag point until the next motion event.
+ * This should be called at the beginning of every method that can delete nodes.
+ * Otherwise the invalidated iterator in the dragpoint can cause crashes. */
+void PathManipulator::hideDragPoint()
+{
+ _dragpoint->setVisible(false);
+ _dragpoint->setIterator(NodeList::iterator());
+}
+
+/** Insert a node in the segment beginning with the supplied iterator,
+ * at the given time value */
+NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t)
+{
+ if (!first) throw std::invalid_argument("Subdivide after invalid iterator");
+ NodeList &list = NodeList::get(first);
+ NodeList::iterator second = first.next();
+ if (!second) throw std::invalid_argument("Subdivide after last node in open path");
+ if (first->type() == NODE_SYMMETRIC)
+ first->setType(NODE_SMOOTH, false);
+ if (second->type() == NODE_SYMMETRIC)
+ second->setType(NODE_SMOOTH, false);
+
+ // We need to insert the segment after 'first'. We can't simply use 'second'
+ // as the point of insertion, because when 'first' is the last node of closed path,
+ // the new node will be inserted as the first node instead.
+ NodeList::iterator insert_at = first;
+ ++insert_at;
+
+ NodeList::iterator inserted;
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ // for a line segment, insert a cusp node
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data,
+ Geom::lerp(t, first->position(), second->position()));
+ n->setType(NODE_CUSP, false);
+ inserted = list.insert(insert_at, n);
+ } else {
+ // build bezier curve and subdivide
+ Geom::CubicBezier temp(first->position(), first->front()->position(),
+ second->back()->position(), second->position());
+ std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t);
+ std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints();
+
+ // set new handle positions
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]);
+ if(!_isBSpline()){
+ n->back()->setPosition(seg1[2]);
+ n->front()->setPosition(seg2[1]);
+ n->setType(NODE_SMOOTH, false);
+ } else {
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ if(second->back()->isDegenerate()){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(second->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ Geom::Point next = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ next = Geom::Point(next[Geom::X] + HANDLE_CUBIC_GAP,next[Geom::Y] + HANDLE_CUBIC_GAP);
+ line_inside_nodes->reset();
+ n->front()->setPosition(next);
+ }else{
+ n->front()->setPosition(seg2[1]);
+ }
+ if(first->front()->isDegenerate()){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(first->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ Geom::Point previous = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ previous = Geom::Point(previous[Geom::X] + HANDLE_CUBIC_GAP,previous[Geom::Y] + HANDLE_CUBIC_GAP);
+ n->back()->setPosition(previous);
+ }else{
+ n->back()->setPosition(seg1[2]);
+ }
+ n->setType(NODE_CUSP, false);
+ }
+ inserted = list.insert(insert_at, n);
+
+ first->front()->move(seg1[1]);
+ second->back()->move(seg2[2]);
+ }
+ return inserted;
+}
+
+/** Find the node that is closest/farthest from the origin
+ * @param origin Point of reference
+ * @param search_selected Consider selected nodes
+ * @param search_unselected Consider unselected nodes
+ * @param closest If true, return closest node, if false, return farthest
+ * @return The matching node, or an empty iterator if none found
+ */
+NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected,
+ bool search_unselected, bool closest)
+{
+ NodeList::iterator match;
+ double extr_dist = closest ? HUGE_VAL : -HUGE_VAL;
+ if (_selection.empty() && !search_unselected) return match;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if(j->selected()) {
+ if (!search_selected) continue;
+ } else {
+ if (!search_unselected) continue;
+ }
+ double dist = Geom::distance(*j, *origin);
+ bool cond = closest ? (dist < extr_dist) : (dist > extr_dist);
+ if (cond) {
+ match = j;
+ extr_dist = dist;
+ }
+ }
+ }
+ return match;
+}
+
+/** Called by the XML observer when something else than us modifies the path. */
+void PathManipulator::_externalChange(unsigned type)
+{
+ hideDragPoint();
+
+ switch (type) {
+ case PATH_CHANGE_D: {
+ _getGeometry();
+
+ // ugly: stored offsets of selected nodes in a vector
+ // vector<bool> should be specialized so that it takes only 1 bit per value
+ std::vector<bool> selpos;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ selpos.push_back(j.selected());
+ }
+ }
+ unsigned size = selpos.size(), curpos = 0;
+
+ _createControlPointsFromGeometry();
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (curpos >= size) goto end_restore;
+ if (selpos[curpos]) _selection.insert(j.ptr());
+ ++curpos;
+ }
+ }
+ end_restore:
+
+ _updateOutline();
+ } break;
+ case PATH_CHANGE_TRANSFORM: {
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (path) {
+ Geom::Affine i2d_change = _d2i_transform;
+ _i2d_transform = path->i2dt_affine();
+ _d2i_transform = _i2d_transform.inverse();
+ i2d_change *= _i2d_transform;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(i2d_change);
+ }
+ }
+ _updateOutline();
+ }
+ } break;
+ default: break;
+ }
+}
+
+/** Create nodes and handles based on the XML of the edited path. */
+void PathManipulator::_createControlPointsFromGeometry()
+{
+ clear();
+
+ // sanitize pathvector and store it in SPCurve,
+ // so that _updateDragPoint doesn't crash on paths with naked movetos
+ Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector());
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+ _spcurve->set_pathvector(pathv);
+
+ pathv *= (_edit_transform * _i2d_transform);
+
+ // in this loop, we know that there are no zero-segment subpaths
+ for (auto & pit : pathv) {
+ // prepare new subpath
+ SubpathPtr subpath(new NodeList(_subpaths));
+ _subpaths.push_back(subpath);
+
+ Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint());
+ subpath->push_back(previous_node);
+
+ bool closed = pit.closed();
+
+ for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) {
+ Geom::Point pos = cit->finalPoint();
+ Node *current_node;
+ // if the closing segment is degenerate and the path is closed, we need to move
+ // the handle of the first node instead of creating a new one
+ if (closed && cit == --(pit.end())) {
+ current_node = subpath->begin().get_pointer();
+ } else {
+ /* regardless of segment type, create a new node at the end
+ * of this segment (unless this is the last segment of a closed path
+ * with a degenerate closing segment */
+ current_node = new Node(_multi_path_manipulator._path_data.node_data, pos);
+ subpath->push_back(current_node);
+ }
+ // if this is a bezier segment, move handles appropriately
+ // TODO: I don't know why the dynamic cast below doesn't want to work
+ // when I replace BezierCurve with CubicBezier. Might be a bug
+ // somewhere in pathv_to_linear_and_cubic_beziers
+ Geom::BezierCurve const *bezier = dynamic_cast<Geom::BezierCurve const*>(&*cit);
+ if (bezier && bezier->order() == 3)
+ {
+ previous_node->front()->setPosition((*bezier)[1]);
+ current_node ->back() ->setPosition((*bezier)[2]);
+ }
+ previous_node = current_node;
+ }
+ // If the path is closed, make the list cyclic
+ if (pit.closed()) subpath->setClosed(true);
+ }
+
+ // we need to set the nodetypes after all the handles are in place,
+ // so that pickBestType works correctly
+ // TODO maybe migrate to inkscape:node-types?
+ // TODO move this into SPPath - do not manipulate directly
+
+ //XML Tree being used here directly while it shouldn't be.
+ gchar const *nts_raw = _path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr;
+ /* Calculate the needed length of the nodetype string.
+ * For closed paths, the entry is duplicated for the starting node,
+ * so we can just use the count of segments including the closing one
+ * to include the extra end node. */
+ /* pad the string to required length with a bogus value.
+ * 'b' and any other letter not recognized by the parser causes the best fit to be set
+ * as the node type */
+ auto const *tsi = nts_raw ? nts_raw : "";
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ char nodetype = (*tsi) ? (*tsi++) : 'b';
+ j.setType(Node::parse_nodetype(nodetype), false);
+ }
+ if (_subpath->closed() && *tsi) {
+ // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of
+ // the first one to remain backward compatible.
+ _subpath->begin()->setType(Node::parse_nodetype(*tsi++), false);
+ }
+ }
+}
+
+//determines if the trace has a bspline effect and the number of steps that it takes
+int PathManipulator::_bsplineGetSteps() const {
+
+ LivePathEffect::LPEBSpline const *lpe_bsp = nullptr;
+
+ SPLPEItem * path = dynamic_cast<SPLPEItem *>(_path);
+ if (path){
+ if(path->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect const *this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if(this_effect){
+ lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline const*>(this_effect->getLPEObj()->get_lpe());
+ }
+ }
+ }
+ int steps = 0;
+ if(lpe_bsp){
+ steps = lpe_bsp->steps+1;
+ }
+ return steps;
+}
+
+// determines if the trace has bspline effect
+void PathManipulator::_recalculateIsBSpline(){
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect const *this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if(this_effect){
+ _is_bspline = true;
+ return;
+ }
+ }
+ _is_bspline = false;
+}
+
+bool PathManipulator::_isBSpline() const {
+ return _is_bspline;
+}
+
+// returns the corresponding strength to the position of the handlers
+double PathManipulator::_bsplineHandlePosition(Handle *h, bool check_other)
+{
+ using Geom::X;
+ using Geom::Y;
+ double pos = NO_POWER;
+ Node *n = h->parent();
+ Node * next_node = nullptr;
+ next_node = n->nodeToward(h);
+ if(next_node){
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(next_node->position());
+ if(!are_near(h->position(), n->position())){
+ pos = Geom::nearest_time(Geom::Point(h->position()[X] - HANDLE_CUBIC_GAP, h->position()[Y] - HANDLE_CUBIC_GAP), *line_inside_nodes->first_segment());
+ }
+ }
+ if (pos == NO_POWER && check_other){
+ return _bsplineHandlePosition(h->other(), false);
+ }
+ return pos;
+}
+
+// give the location for the handler in the corresponding position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h, bool check_other)
+{
+ double pos = this->_bsplineHandlePosition(h, check_other);
+ return _bsplineHandleReposition(h,pos);
+}
+
+// give the location for the handler to the specified position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h,double pos){
+ using Geom::X;
+ using Geom::Y;
+ Geom::Point ret = h->position();
+ Node *n = h->parent();
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ Node * next_node = nullptr;
+ next_node = n->nodeToward(h);
+ if(next_node && pos != NO_POWER){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(next_node->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ ret = sbasis_inside_nodes.valueAt(pos);
+ ret = Geom::Point(ret[X] + HANDLE_CUBIC_GAP, ret[Y] + HANDLE_CUBIC_GAP);
+ }else{
+ if(pos == NO_POWER){
+ ret = n->position();
+ }
+ }
+ return ret;
+}
+
+/** Construct the geometric representation of nodes and handles, update the outline
+ * and display
+ * \param alert_LPE if true, first the LPE is warned what the new path is going to be before updating it
+ */
+void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE)
+{
+ Geom::PathBuilder builder;
+ //Refresh if is bspline some times -think on path change selection, this value get lost
+ _recalculateIsBSpline();
+ for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) {
+ SubpathPtr subpath = *spi;
+ if (subpath->empty()) {
+ _subpaths.erase(spi++);
+ continue;
+ }
+ NodeList::iterator prev = subpath->begin();
+ builder.moveTo(prev->position());
+ for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) {
+ build_segment(builder, prev.ptr(), i.ptr());
+ prev = i;
+ }
+ if (subpath->closed()) {
+ // Here we link the last and first node if the path is closed.
+ // If the last segment is Bezier, we add it.
+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
+ build_segment(builder, prev.ptr(), subpath->begin().ptr());
+ }
+ // if that segment is linear, we just call closePath().
+ builder.closePath();
+ }
+ ++spi;
+ }
+ builder.flush();
+ Geom::PathVector pathv = builder.peek() * (_edit_transform * _i2d_transform).inverse();
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+
+ if (_spcurve->get_pathvector() == pathv) {
+ return;
+ }
+ _spcurve->set_pathvector(pathv);
+ if (alert_LPE) {
+ /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming?
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect* this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE);
+ if(this_effect){
+ LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect->getLPEObj()->get_lpe());
+ if (lpe_pwr) {
+ lpe_pwr->adjustForNewPath(pathv);
+ }
+ }
+ }
+ }
+ if (_live_outline) {
+ _updateOutline();
+ }
+ if (_live_objects) {
+ _setGeometry();
+ }
+}
+
+/** Build one segment of the geometric representation.
+ * @relates PathManipulator */
+void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node)
+{
+ if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate())
+ {
+ // NOTE: It seems like the renderer cannot correctly handle vline / hline segments,
+ // and trying to display a path using them results in funny artifacts.
+ builder.lineTo(cur_node->position());
+ } else {
+ // this is a bezier segment
+ builder.curveTo(
+ prev_node->front()->position(),
+ cur_node->back()->position(),
+ cur_node->position());
+ }
+}
+
+/** Construct a node type string to store in the sodipodi:nodetypes attribute. */
+std::string PathManipulator::_createTypeString()
+{
+ // precondition: no single-node subpaths
+ std::stringstream tstr;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ tstr << j.type();
+ }
+ // nodestring format peculiarity: first node is counted twice for closed paths
+ if (_subpath->closed()) tstr << _subpath->begin()->type();
+ }
+ return tstr.str();
+}
+
+/** Update the path outline. */
+void PathManipulator::_updateOutline()
+{
+ if (!_show_outline) {
+ sp_canvas_item_hide(_outline);
+ return;
+ }
+
+ Geom::PathVector pv = _spcurve->get_pathvector();
+ pv *= (_edit_transform * _i2d_transform);
+ // This SPCurve thing has to be killed with extreme prejudice
+ SPCurve *_hc = new SPCurve();
+ if (_show_path_direction) {
+ // To show the direction, we append additional subpaths which consist of a single
+ // linear segment that starts at the time value of 0.5 and extends for 10 pixels
+ // at an angle 150 degrees from the unit tangent. This creates the appearance
+ // of little 'harpoons' that show the direction of the subpaths.
+ auto rot_scale_w2d = Geom::Rotate(210.0 / 180.0 * M_PI) * Geom::Scale(10.0) * _desktop->w2d();
+ Geom::PathVector arrows;
+ for (auto & path : pv) {
+ for (Geom::Path::iterator j = path.begin(); j != path.end_default(); ++j) {
+ Geom::Point at = j->pointAt(0.5);
+ Geom::Point ut = j->unitTangentAt(0.5);
+ Geom::Point arrow_end = at + (Geom::unit_vector(_desktop->d2w(ut)) * rot_scale_w2d);
+
+ Geom::Path arrow(at);
+ arrow.appendNew<Geom::LineSegment>(arrow_end);
+ arrows.push_back(arrow);
+ }
+ }
+ pv.insert(pv.end(), arrows.begin(), arrows.end());
+ }
+ _hc->set_pathvector(pv);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(_outline), _hc);
+ sp_canvas_item_show(_outline);
+ _hc->unref();
+}
+
+/** Retrieve the geometry of the edited object from the object tree */
+void PathManipulator::_getGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (lpeobj) {
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ _spcurve->unref();
+ _spcurve = new SPCurve(pathparam->get_pathvector());
+ }
+ } else if (path) {
+ _spcurve->unref();
+ _spcurve = path->getCurveForEdit();
+ // never allow NULL to sneak in here!
+ if (_spcurve == nullptr) {
+ _spcurve = new SPCurve();
+ }
+ }
+}
+
+/** Set the geometry of the edited object in the object tree, but do not commit to XML */
+void PathManipulator::_setGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (lpeobj) {
+ // copied from nodepath.cpp
+ // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is
+ // a LivePathEffectObject. (mad laughter)
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ if (pathparam->get_pathvector() == _spcurve->get_pathvector()) {
+ return; //False we dont update LPE
+ }
+ pathparam->set_new_value(_spcurve->get_pathvector(), false);
+ lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ }
+ } else if (path) {
+ // return true to leave the decision on empty to the caller.
+ // Maybe the path become empty and we want to update to empty
+ if (empty()) return;
+ if (path->getCurveBeforeLPE(true)) {
+ if (!_spcurve->is_equal(path->getCurveBeforeLPE(true))) {
+ path->setCurveBeforeLPE(_spcurve);
+ sp_lpe_item_update_patheffect(path, true, false);
+ }
+ } else if(!_spcurve->is_equal(path->getCurve(true))) {
+ path->setCurve(_spcurve);
+ }
+ }
+}
+
+/** Figure out in what attribute to store the nodetype string. */
+Glib::ustring PathManipulator::_nodetypesKey()
+{
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ if (!lpeobj) {
+ return "sodipodi:nodetypes";
+ } else {
+ return _lpe_key + "-nodetypes";
+ }
+}
+
+/** Return the XML node we are editing.
+ * This method is wrong but necessary at the moment. */
+Inkscape::XML::Node *PathManipulator::_getXMLNode()
+{
+ //XML Tree being used here directly while it shouldn't be.
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ if (!lpeobj)
+ return _path->getRepr();
+ //XML Tree being used here directly while it shouldn't be.
+ return lpeobj->getRepr();
+}
+
+bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event)
+{
+ if (event->button != 1) return false;
+ if (held_alt(*event) && held_control(*event)) {
+ // Ctrl+Alt+click: delete nodes
+ hideDragPoint();
+ NodeList::iterator iter = NodeList::get_iterator(n);
+ NodeList &nl = iter->nodeList();
+
+ if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) {
+ // Removing last node of closed path - delete it
+ nl.kill();
+ } else {
+ // In other cases, delete the node under cursor
+ _deleteStretch(iter, iter.next(), true);
+ }
+
+ if (!empty()) {
+ update(true);
+ }
+
+ // We need to call MPM's method because it could have been our last node
+ _multi_path_manipulator._doneWithCleanup(_("Delete node"));
+
+ return true;
+ } else if (held_control(*event)) {
+ // Ctrl+click: cycle between node types
+ if (!n->isEndNode()) {
+ n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE));
+ update();
+ _commit(_("Cycle node type"));
+ }
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_handleGrabbed()
+{
+ _selection.hideTransformHandles();
+}
+
+void PathManipulator::_handleUngrabbed()
+{
+ _selection.restoreTransformHandles();
+ _commit(_("Drag handle"));
+}
+
+bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event)
+{
+ // retracting by Ctrl+click
+ if (event->button == 1 && held_control(*event)) {
+ h->move(h->parent()->position());
+ update();
+ _commit(_("Retract handle"));
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected) {
+ for (auto & n : pvec) {
+ _selectionChanged(n, selected);
+ }
+}
+
+void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected)
+{
+ // don't do anything if we do not show handles
+ if (!_show_handles) return;
+
+ // only do something if a node changed selection state
+ Node *node = dynamic_cast<Node*>(p);
+ if (!node) return;
+
+ // update handle display
+ NodeList::iterator iters[5];
+ iters[2] = NodeList::get_iterator(node);
+ iters[1] = iters[2].prev();
+ iters[3] = iters[2].next();
+ if (selected) {
+ // selection - show handles on this node and adjacent ones
+ node->showHandles(true);
+ if (iters[1]) iters[1]->showHandles(true);
+ if (iters[3]) iters[3]->showHandles(true);
+ } else {
+ /* Deselection is more complex.
+ * The change might affect 3 nodes - this one and two adjacent.
+ * If the node and both its neighbors are deselected, hide handles.
+ * Otherwise, leave as is. */
+ if (iters[1]) iters[0] = iters[1].prev();
+ if (iters[3]) iters[4] = iters[3].next();
+ bool nodesel[5];
+ for (int i = 0; i < 5; ++i) {
+ nodesel[i] = iters[i] && iters[i]->selected();
+ }
+ for (int i = 1; i < 4; ++i) {
+ if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) {
+ iters[i]->showHandles(false);
+ }
+ }
+ }
+}
+
+/** Removes all nodes belonging to this manipulator from the control point selection */
+void PathManipulator::_removeNodesFromSelection()
+{
+ // remove this manipulator's nodes from selection
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ _selection.erase(j.get_pointer());
+ }
+ }
+}
+
+/** Update the XML representation and put the specified annotation on the undo stack */
+void PathManipulator::_commit(Glib::ustring const &annotation)
+{
+ writeXML();
+ if (_desktop) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, annotation.data());
+ }
+}
+
+void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key)
+{
+ writeXML();
+ DocumentUndo::maybeDone(_desktop->getDocument(), key, SP_VERB_CONTEXT_NODE,
+ annotation.data());
+}
+
+/** Update the position of the curve drag point such that it is over the nearest
+ * point of the path. */
+Geom::Coord PathManipulator::_updateDragPoint(Geom::Point const &evp)
+{
+ Geom::Coord dist = HUGE_VAL;
+
+ Geom::Affine to_desktop = _edit_transform * _i2d_transform;
+ Geom::PathVector pv = _spcurve->get_pathvector();
+ boost::optional<Geom::PathVectorTime> pvp =
+ pv.nearestTime(_desktop->w2d(evp) * to_desktop.inverse());
+ if (!pvp) return dist;
+ Geom::Point nearest_pt = _desktop->d2w(pv.pointAt(*pvp) * to_desktop);
+
+ double fracpart = pvp->t;
+ std::list<SubpathPtr>::iterator spi = _subpaths.begin();
+ for (unsigned i = 0; i < pvp->path_index; ++i, ++spi) {}
+ NodeList::iterator first = (*spi)->before(pvp->asPathTime());
+
+ dist = Geom::distance(evp, nearest_pt);
+
+ double stroke_tolerance = _getStrokeTolerance();
+ if (first && first.next() &&
+ fracpart != 0.0 &&
+ fracpart != 1.0 &&
+ dist < stroke_tolerance)
+ {
+ _dragpoint->setVisible(true);
+ _dragpoint->setPosition(_desktop->w2d(nearest_pt));
+ _dragpoint->setSize(2 * stroke_tolerance);
+ _dragpoint->setTimeValue(fracpart);
+ _dragpoint->setIterator(first);
+ } else {
+ _dragpoint->setVisible(false);
+ }
+
+ return dist;
+}
+
+/// This is called on zoom change to update the direction arrows
+void PathManipulator::_updateOutlineOnZoomChange()
+{
+ if (_show_path_direction) _updateOutline();
+}
+
+/** Compute the radius from the edge of the path where clicks should initiate a curve drag
+ * or segment selection, in window coordinates. */
+double PathManipulator::_getStrokeTolerance()
+{
+ /* Stroke event tolerance is equal to half the stroke's width plus the global
+ * drag tolerance setting. */
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100);
+ if (_path && _path->style && !_path->style->stroke.isNone()) {
+ ret += _path->style->stroke_width.computed * 0.5
+ * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords
+ * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords
+ }
+ return ret;
+}
+
+} // 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/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h
new file mode 100644
index 0000000..3d7fbd5
--- /dev/null
+++ b/src/ui/tool/path-manipulator.h
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Path manipulator - a component that edits a single path on-canvas
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_PATH_MANIPULATOR_H
+#define SEEN_UI_TOOL_PATH_MANIPULATOR_H
+
+#include <string>
+#include <memory>
+#include <2geom/pathvector.h>
+#include <2geom/affine.h>
+#include "ui/tool/node.h"
+#include "ui/tool/manipulator.h"
+#include "live_effects/lpe-bspline.h"
+
+struct SPCanvasItem;
+class SPCurve;
+class SPPath;
+
+namespace Inkscape {
+namespace XML { class Node; }
+
+namespace UI {
+
+class PathManipulator;
+class ControlPointSelection;
+class PathManipulatorObserver;
+class CurveDragPoint;
+class PathCanvasGroups;
+class MultiPathManipulator;
+class Node;
+class Handle;
+
+struct PathSharedData {
+ NodeSharedData node_data;
+ SPCanvasGroup *outline_group;
+ SPCanvasGroup *dragpoint_group;
+};
+
+/**
+ * Manipulator that edits a single path using nodes with handles.
+ * Currently only cubic bezier and linear segments are supported, but this might change
+ * some time in the future.
+ */
+class PathManipulator : public PointManipulator {
+public:
+ typedef SPPath *ItemType;
+
+ PathManipulator(MultiPathManipulator &mpm, SPObject *path, Geom::Affine const &edit_trans,
+ guint32 outline_color, Glib::ustring lpe_key);
+ ~PathManipulator() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ bool empty();
+ void writeXML();
+ void update(bool alert_LPE = false); // update display, but don't commit
+ void clear(); // remove all nodes from manipulator
+ SPObject *item() { return _path; }
+
+ void selectSubpaths();
+ void invertSelectionInSubpaths();
+
+ void insertNodeAtExtremum(ExtremumType extremum);
+ void insertNodes();
+ void insertNode(Geom::Point);
+ void insertNode(NodeList::iterator first, double t, bool take_selection);
+ void duplicateNodes();
+ void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator());
+ void weldSegments();
+ void breakNodes();
+ void deleteNodes(bool keep_shape = true);
+ void deleteSegments();
+ void reverseSubpaths(bool selected_only);
+ void setSegmentType(SegmentType);
+
+ void scaleHandle(Node *n, int which, int dir, bool pixel);
+ void rotateHandle(Node *n, int which, int dir, bool pixel);
+
+ void showOutline(bool show);
+ void showHandles(bool show);
+ void showPathDirection(bool show);
+ void setLiveOutline(bool set);
+ void setLiveObjects(bool set);
+ void updateHandles();
+ void setControlsTransform(Geom::Affine const &);
+ void hideDragPoint();
+ MultiPathManipulator &mpm() { return _multi_path_manipulator; }
+
+ NodeList::iterator subdivideSegment(NodeList::iterator after, double t);
+ NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected,
+ bool search_unselected, bool closest);
+
+ int _bsplineGetSteps() const;
+ // this is necessary for Tab-selection in MultiPathManipulator
+ SubpathList &subpathList() { return _subpaths; }
+
+ static bool is_item_type(void *item);
+private:
+ typedef NodeList Subpath;
+ typedef std::shared_ptr<NodeList> SubpathPtr;
+
+ void _createControlPointsFromGeometry();
+
+ void _recalculateIsBSpline();
+ bool _isBSpline() const;
+ double _bsplineHandlePosition(Handle *h, bool check_other = true);
+ Geom::Point _bsplineHandleReposition(Handle *h, bool check_other = true);
+ Geom::Point _bsplineHandleReposition(Handle *h, double pos);
+ void _createGeometryFromControlPoints(bool alert_LPE = false);
+ unsigned _deleteStretch(NodeList::iterator first, NodeList::iterator last, bool keep_shape);
+ std::string _createTypeString();
+ void _updateOutline();
+ //void _setOutline(Geom::PathVector const &);
+ void _getGeometry();
+ void _setGeometry();
+ Glib::ustring _nodetypesKey();
+ Inkscape::XML::Node *_getXMLNode();
+
+ void _selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected);
+ void _selectionChanged(SelectableControlPoint * p, bool selected);
+ bool _nodeClicked(Node *, GdkEventButton *);
+ void _handleGrabbed();
+ bool _handleClicked(Handle *, GdkEventButton *);
+ void _handleUngrabbed();
+
+ void _externalChange(unsigned type);
+ void _removeNodesFromSelection();
+ void _commit(Glib::ustring const &annotation);
+ void _commit(Glib::ustring const &annotation, gchar const *key);
+ Geom::Coord _updateDragPoint(Geom::Point const &);
+ void _updateOutlineOnZoomChange();
+ double _getStrokeTolerance();
+ Handle *_chooseHandle(Node *n, int which);
+
+ SubpathList _subpaths;
+ MultiPathManipulator &_multi_path_manipulator;
+ SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!!
+ SPCurve *_spcurve; // in item coordinates
+ SPCanvasItem *_outline;
+ CurveDragPoint *_dragpoint; // an invisible control point hovering over curve
+ PathManipulatorObserver *_observer;
+ Geom::Affine _d2i_transform; ///< desktop-to-item transform
+ Geom::Affine _i2d_transform; ///< item-to-desktop transform, inverse of _d2i_transform
+ Geom::Affine _edit_transform; ///< additional transform to apply to editing controls
+ bool _show_handles;
+ bool _show_outline;
+ bool _show_path_direction;
+ bool _live_outline;
+ bool _live_objects;
+ bool _is_bspline;
+ Glib::ustring _lpe_key;
+
+ friend class PathManipulatorObserver;
+ friend class CurveDragPoint;
+ friend class Node;
+ friend class Handle;
+};
+
+} // 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/tool/selectable-control-point.cpp b/src/ui/tool/selectable-control-point.cpp
new file mode 100644
index 0000000..02480ec
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.cpp
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tool/selectable-control-point.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+
+namespace Inkscape {
+namespace UI {
+
+ControlPoint::ColorSet SelectableControlPoint::_default_scp_color_set = {
+ {0xffffff00, 0x01000000}, // normal fill, stroke
+ {0xff0000ff, 0x01000000}, // mouseover fill, stroke
+ {0x0000ffff, 0x01000000}, // clicked fill, stroke
+ //
+ {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected
+ {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected
+ {0xff000000, 0x000000ff} // clicked fill, stroke when selected
+};
+
+SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::ControlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset, SPCanvasGroup *group) :
+ ControlPoint(d, initial_pos, anchor, type, cset, group),
+ _selection(sel)
+{
+ _selection.allPoints().insert(this);
+}
+
+SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ControlPointSelection &sel,
+ ColorSet const &cset, SPCanvasGroup *group) :
+ ControlPoint(d, initial_pos, anchor, pixbuf, cset, group),
+ _selection (sel)
+{
+ _selection.allPoints().insert(this);
+}
+
+SelectableControlPoint::~SelectableControlPoint()
+{
+ _selection.erase(this);
+ _selection.allPoints().erase(this);
+}
+
+bool SelectableControlPoint::grabbed(GdkEventMotion *)
+{
+ // if a point is dragged while not selected, it should select itself
+ if (!selected()) {
+ _takeSelection();
+ }
+ _selection._pointGrabbed(this);
+ return false;
+}
+
+void SelectableControlPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ _selection._pointDragged(new_pos, event);
+}
+
+void SelectableControlPoint::ungrabbed(GdkEventButton *)
+{
+ _selection._pointUngrabbed();
+}
+
+bool SelectableControlPoint::clicked(GdkEventButton *event)
+{
+ if (_selection._pointClicked(this, event))
+ return true;
+
+ if (event->button != 1) return false;
+ if (held_shift(*event)) {
+ if (selected()) {
+ _selection.erase(this);
+ } else {
+ _selection.insert(this);
+ }
+ } else {
+ _takeSelection();
+ }
+ return true;
+}
+
+void SelectableControlPoint::select(bool toselect)
+{
+ if (toselect) {
+ _selection.insert(this);
+ } else {
+ _selection.erase(this);
+ }
+}
+
+void SelectableControlPoint::_takeSelection()
+{
+ _selection.clear();
+ _selection.insert(this);
+}
+
+bool SelectableControlPoint::selected() const
+{
+ SelectableControlPoint *p = const_cast<SelectableControlPoint*>(this);
+ return _selection.find(p) != _selection.end();
+}
+
+void SelectableControlPoint::_setState(State state)
+{
+ if (!selected()) {
+ ControlPoint::_setState(state);
+ } else {
+ ColorEntry current = {0, 0};
+ ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset;
+ switch (state) {
+ case STATE_NORMAL:
+ current = activeCset.selected_normal;
+ break;
+ case STATE_MOUSEOVER:
+ current = activeCset.selected_mouseover;
+ break;
+ case STATE_CLICKED:
+ current = activeCset.selected_clicked;
+ break;
+ }
+ _setColors(current);
+ _state = state;
+ }
+}
+
+} // 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/tool/selectable-control-point.h b/src/ui/tool/selectable-control-point.h
new file mode 100644
index 0000000..919988a
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H
+#define SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H
+
+#include "ui/tool/control-point.h"
+
+namespace Inkscape {
+namespace UI {
+
+class ControlPointSelection;
+
+/**
+ * Desktop-bound selectable control object.
+ */
+class SelectableControlPoint : public ControlPoint {
+public:
+
+ ~SelectableControlPoint() override;
+ bool selected() const;
+ void updateState() { _setState(_state); }
+ virtual Geom::Rect bounds() const {
+ return Geom::Rect(position(), position());
+ }
+ virtual void select(bool toselect);
+ friend class NodeList;
+
+
+protected:
+
+ SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::ControlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset = _default_scp_color_set, SPCanvasGroup *group = nullptr);
+
+ SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ ControlPointSelection &sel,
+ ColorSet const &cset = _default_scp_color_set, SPCanvasGroup *group = nullptr);
+
+ void _setState(State state) override;
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ bool grabbed(GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *event) override;
+ bool clicked(GdkEventButton *event) override;
+
+ ControlPointSelection &_selection;
+
+private:
+
+ void _takeSelection();
+
+ static ColorSet _default_scp_color_set;
+};
+
+} // 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/tool/selector.cpp b/src/ui/tool/selector.cpp
new file mode 100644
index 0000000..934d623
--- /dev/null
+++ b/src/ui/tool/selector.cpp
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Selector component (click and rubberband)
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "control-point.h"
+#include "desktop.h"
+
+#include "display/sodipodi-ctrlrect.h"
+#include "ui/tools/tool-base.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/selector.h"
+
+#include <gdk/gdkkeysyms.h>
+
+namespace Inkscape {
+namespace UI {
+
+/** A hidden control point used for rubberbanding and selection.
+ * It uses a clever hack: the canvas item is hidden and only receives events when they
+ * are passed to it using Selector's event() function. When left mouse button
+ * is pressed, it grabs events and handles drags and clicks in the usual way. */
+class SelectorPoint : public ControlPoint {
+public:
+ SelectorPoint(SPDesktop *d, SPCanvasGroup *group, Selector *s) :
+ ControlPoint(d, Geom::Point(0,0), SP_ANCHOR_CENTER,
+ CTRL_TYPE_INVISIPOINT,
+ invisible_cset, group),
+ _selector(s),
+ _cancel(false)
+ {
+ setVisible(false);
+ _rubber = static_cast<CtrlRect*>(sp_canvas_item_new(_desktop->getControls(),
+ SP_TYPE_CTRLRECT, nullptr));
+ _rubber->setColor(0x8080ffff, false, 0x0);
+ _rubber->setInvert(true);
+ sp_canvas_item_hide(_rubber);
+ }
+
+ ~SelectorPoint() override {
+ sp_canvas_item_destroy(_rubber);
+ }
+
+ SPDesktop *desktop() { return _desktop; }
+
+ bool event(Inkscape::UI::Tools::ToolBase *ec, GdkEvent *e) {
+ return _eventHandler(ec, e);
+ }
+
+protected:
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override {
+ if (event->type == GDK_KEY_PRESS && shortcut_key(event->key) == GDK_KEY_Escape &&
+ sp_canvas_item_is_visible(_rubber))
+ {
+ _cancel = true;
+ sp_canvas_item_hide(_rubber);
+ return true;
+ }
+ return ControlPoint::_eventHandler(event_context, event);
+ }
+
+private:
+ bool grabbed(GdkEventMotion *) override {
+ _cancel = false;
+ _start = position();
+ sp_canvas_item_show(_rubber);
+ return false;
+ }
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *) override {
+ if (_cancel) return;
+ Geom::Rect sel(_start, new_pos);
+ _rubber->setRectangle(sel);
+ }
+
+ void ungrabbed(GdkEventButton *event) override {
+ if (_cancel) return;
+ sp_canvas_item_hide(_rubber);
+ Geom::Rect sel(_start, position());
+ _selector->signal_area.emit(sel, event);
+ }
+
+ bool clicked(GdkEventButton *event) override {
+ if (event->button != 1) return false;
+ _selector->signal_point.emit(position(), event);
+ return true;
+ }
+
+ CtrlRect *_rubber;
+ Selector *_selector;
+ Geom::Point _start;
+ bool _cancel;
+};
+
+
+Selector::Selector(SPDesktop *d)
+ : Manipulator(d)
+ , _dragger(new SelectorPoint(d, d->getControls(), this))
+{
+ _dragger->setVisible(false);
+}
+
+Selector::~Selector()
+{
+ delete _dragger;
+}
+
+bool Selector::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // The hidden control point will capture all events after it obtains the grab,
+ // but it relies on this function to initiate it. If we pass only first button
+ // press events here, it won't interfere with any other event handling.
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ // Do not pass button presses other than left button to the control point.
+ // This way middle click and right click can be handled in ToolBase.
+ if (event->button.button == 1 && !event_context->space_panning) {
+ _dragger->setPosition(_desktop->w2d(event_point(event->motion)));
+ return _dragger->event(event_context, event);
+ }
+ break;
+ default: break;
+ }
+ return false;
+}
+
+bool Selector::doubleClicked() {
+ return _dragger->doubleClicked();
+}
+
+} // 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/tool/selector.h b/src/ui/tool/selector.h
new file mode 100644
index 0000000..6881a63
--- /dev/null
+++ b/src/ui/tool/selector.h
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Selector component (click and rubberband)
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SELECTOR_H
+#define SEEN_UI_TOOL_SELECTOR_H
+
+#include <memory>
+#include <gdk/gdk.h>
+#include <2geom/rect.h>
+#include "ui/tool/manipulator.h"
+
+class SPDesktop;
+class CtrlRect;
+
+namespace Inkscape {
+namespace UI {
+
+class SelectorPoint;
+
+class Selector : public Manipulator {
+public:
+ Selector(SPDesktop *d);
+ ~Selector() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+ virtual bool doubleClicked();
+
+ sigc::signal<void, Geom::Rect const &, GdkEventButton*> signal_area;
+ sigc::signal<void, Geom::Point const &, GdkEventButton*> signal_point;
+private:
+ SelectorPoint *_dragger;
+ Geom::Point _start;
+ CtrlRect *_rubber;
+ gulong _connection;
+ bool _cancel;
+ friend class SelectorPoint;
+};
+
+} // 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/tool/shape-record.h b/src/ui/tool/shape-record.h
new file mode 100644
index 0000000..d46a1c0
--- /dev/null
+++ b/src/ui/tool/shape-record.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Structures that store data needed for shape editing which are not contained
+ * directly in the XML node
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SHAPE_RECORD_H
+#define SEEN_UI_TOOL_SHAPE_RECORD_H
+
+#include <glibmm/ustring.h>
+#include <boost/operators.hpp>
+#include <2geom/affine.h>
+
+class SPItem;
+namespace Inkscape {
+namespace UI {
+
+/** Role of the shape in the drawing - affects outline display and color */
+enum ShapeRole {
+ SHAPE_ROLE_NORMAL,
+ SHAPE_ROLE_CLIPPING_PATH,
+ SHAPE_ROLE_MASK,
+ SHAPE_ROLE_LPE_PARAM // implies edit_original set to true in ShapeRecord
+};
+
+struct ShapeRecord :
+ public boost::totally_ordered<ShapeRecord>
+{
+ SPObject *object; // SP node for the edited shape could be a lpeoject invisible so we use a spobject
+ Geom::Affine edit_transform; // how to transform controls - used for clipping paths and masks
+ ShapeRole role;
+ Glib::ustring lpe_key; // name of LPE shape param being edited
+
+ inline bool operator==(ShapeRecord const &o) const {
+ return object == o.object && lpe_key == o.lpe_key;
+ }
+ inline bool operator<(ShapeRecord const &o) const {
+ return object == o.object ? (lpe_key < o.lpe_key) : (object < o.object);
+ }
+};
+
+} // 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/tool/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp
new file mode 100644
index 0000000..c1d6692
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.cpp
@@ -0,0 +1,867 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Affine transform handles component
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+#include <algorithm>
+
+#include <glib/gi18n.h>
+
+#include <2geom/transforms.h>
+
+#include "control-point.h"
+#include "desktop.h"
+#include "pure-transform.h"
+#include "seltrans.h"
+#include "snap.h"
+
+#include "display/sodipodi-ctrlrect.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/tool/commit-events.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/node.h"
+#include "ui/tool/selectable-control-point.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/tools/node-tool.h"
+
+
+// FIXME BRAIN DAMAGE WARNING: this is a global variable in select-context.cpp
+// It should be moved to a header
+extern GdkPixbuf *handles[];
+GType sp_select_context_get_type();
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+
+SPAnchorType corner_to_anchor(unsigned c) {
+ switch (c % 4) {
+ case 0: return SP_ANCHOR_NE;
+ case 1: return SP_ANCHOR_NW;
+ case 2: return SP_ANCHOR_SW;
+ default: return SP_ANCHOR_SE;
+ }
+}
+
+SPAnchorType side_to_anchor(unsigned s) {
+ switch (s % 4) {
+ case 0: return SP_ANCHOR_N;
+ case 1: return SP_ANCHOR_W;
+ case 2: return SP_ANCHOR_S;
+ default: return SP_ANCHOR_E;
+ }
+}
+
+// TODO move those two functions into a common place
+double snap_angle(double a) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double unit_angle = M_PI / snaps;
+ return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI);
+}
+
+double snap_increment_degrees() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ return 180.0 / snaps;
+}
+
+} // anonymous namespace
+
+ControlPoint::ColorSet TransformHandle::thandle_cset = {
+ {0x000000ff, 0x000000ff},
+ {0x00ff6600, 0x000000ff},
+ {0x00ff6600, 0x000000ff},
+ //
+ {0x000000ff, 0x000000ff},
+ {0x00ff6600, 0x000000ff},
+ {0x00ff6600, 0x000000ff}
+};
+
+TransformHandle::TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb) :
+ ControlPoint(th._desktop, Geom::Point(), anchor,
+ pb,
+ thandle_cset, th._transform_handle_group),
+ _th(th)
+{
+ setVisible(false);
+}
+
+// TODO: This code is duplicated in seltrans.cpp; fix this!
+void TransformHandle::getNextClosestPoint(bool reverse)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/snapclosestonly/value", false)) {
+ if (!_all_snap_sources_sorted.empty()) {
+ if (reverse) { // Shift-tab will find a closer point
+ if (_all_snap_sources_iter == _all_snap_sources_sorted.begin()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.end();
+ }
+ --_all_snap_sources_iter;
+ } else { // Tab will find a point further away
+ ++_all_snap_sources_iter;
+ if (_all_snap_sources_iter == _all_snap_sources_sorted.end()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.begin();
+ }
+ }
+
+ _snap_points.clear();
+ _snap_points.push_back(*_all_snap_sources_iter);
+
+ // Show the updated snap source now; otherwise it won't be shown until the selection is being moved again
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop);
+ m.displaySnapsource(*_all_snap_sources_iter);
+ m.unSetup();
+ }
+ }
+}
+
+bool TransformHandle::grabbed(GdkEventMotion *)
+{
+ _origin = position();
+ _last_transform.setIdentity();
+ startTransform();
+
+ _th._setActiveHandle(this);
+ _setLurking(true);
+ _setState(_state);
+
+ // Collect the snap-candidates, one for each selected node. These will be stored in the _snap_points vector.
+ Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context);
+ //ControlPointSelection *selection = nt->_selected_nodes.get();
+ ControlPointSelection* selection = nt->_selected_nodes;
+
+ selection->setOriginalPoints();
+ selection->getOriginalPoints(_snap_points);
+ selection->getUnselectedPoints(_unselected_points);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/snapclosestonly/value", false)) {
+ // Find the closest snap source candidate
+ _all_snap_sources_sorted = _snap_points;
+
+ // Calculate and store the distance to the reference point for each snap candidate point
+ for(auto & i : _all_snap_sources_sorted) {
+ i.setDistance(Geom::L2(i.getPoint() - _origin));
+ }
+
+ // Sort them ascending, using the distance calculated above as the single criteria
+ std::sort(_all_snap_sources_sorted.begin(), _all_snap_sources_sorted.end());
+
+ // Now get the closest snap source
+ _snap_points.clear();
+ if (!_all_snap_sources_sorted.empty()) {
+ _all_snap_sources_iter = _all_snap_sources_sorted.begin();
+ _snap_points.push_back(_all_snap_sources_sorted.front());
+ }
+ }
+
+ return false;
+}
+
+void TransformHandle::dragged(Geom::Point &new_pos, GdkEventMotion *event)
+{
+ Geom::Affine t = computeTransform(new_pos, event);
+ // protect against degeneracies
+ if (t.isSingular()) return;
+ Geom::Affine incr = _last_transform.inverse() * t;
+ if (incr.isSingular()) return;
+ _th.signal_transform.emit(incr);
+ _last_transform = t;
+}
+
+void TransformHandle::ungrabbed(GdkEventButton *)
+{
+ _snap_points.clear();
+ _th._clearActiveHandle();
+ _setLurking(false);
+ _setState(_state);
+ endTransform();
+ _th.signal_commit.emit(getCommitEvent());
+
+ //updates the positions of the nodes
+ Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context);
+ ControlPointSelection* selection = nt->_selected_nodes;
+ selection->setOriginalPoints();
+}
+
+
+class ScaleHandle : public TransformHandle {
+public:
+ ScaleHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb)
+ : TransformHandle(th, anchor, pb)
+ {}
+protected:
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_control(state)) {
+ if (state_held_shift(state)) {
+ return C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: scale uniformly about the rotation center");
+ }
+ return C_("Transform handle tip", "<b>Ctrl:</b> scale uniformly");
+ }
+ if (state_held_shift(state)) {
+ if (state_held_alt(state)) {
+ return C_("Transform handle tip",
+ "<b>Shift+Alt</b>: scale using an integer ratio about the rotation center");
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: scale from the rotation center");
+ }
+ if (state_held_alt(state)) {
+ return C_("Transform handle tip", "<b>Alt</b>: scale using an integer ratio");
+ }
+ return C_("Transform handle tip", "<b>Scale handle</b>: drag to scale the selection");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ return format_tip(C_("Transform handle tip",
+ "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100);
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+ static double _last_scale_x, _last_scale_y;
+};
+double ScaleHandle::_last_scale_x = 1.0;
+double ScaleHandle::_last_scale_y = 1.0;
+
+/**
+ * Corner scaling handle for node transforms.
+ */
+class ScaleCornerHandle : public ScaleHandle {
+public:
+
+ ScaleCornerHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) :
+ ScaleHandle(th, corner_to_anchor(d_corner), _corner_to_pixbuf(d_corner)),
+ _corner(corner)
+ {}
+
+protected:
+ void startTransform() override {
+ _sc_center = _th.rotationCenter();
+ _sc_opposite = _th.bounds().corner(_corner + 2);
+ _last_scale_x = _last_scale_y = 1.0;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override {
+ Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite;
+ Geom::Point vold = _origin - scc, vnew = new_pos - scc;
+ // avoid exploding the selection
+ if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0))
+ return Geom::identity();
+
+ Geom::Scale scale = Geom::Scale(vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y]);
+
+ if (held_alt(*event)) {
+ for (unsigned i = 0; i < 2; ++i) {
+ if (fabs(scale[i]) >= 1.0) {
+ scale[i] = round(scale[i]);
+ } else {
+ scale[i] = 1.0 / round(1.0 / MIN(scale[i],10));
+ }
+ }
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ Inkscape::PureScale *ptr;
+ if (held_control(*event)) {
+ scale[0] = scale[1] = std::min(scale[0], scale[1]);
+ ptr = new Inkscape::PureScaleConstrained(Geom::Scale(scale[0], scale[1]), scc);
+ } else {
+ ptr = new Inkscape::PureScale(Geom::Scale(scale[0], scale[1]), scc, false);
+ }
+ m.snapTransformed(_snap_points, _origin, (*ptr));
+ m.unSetup();
+ if (ptr->best_snapped_point.getSnapped()) {
+ scale = ptr->getScaleSnapped();
+ }
+
+ delete ptr;
+ }
+
+ _last_scale_x = scale[0];
+ _last_scale_y = scale[1];
+ Geom::Affine t = Geom::Translate(-scc)
+ * Geom::Scale(scale[0], scale[1])
+ * Geom::Translate(scc);
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override {
+ return _last_transform.isUniformScale()
+ ? COMMIT_MOUSE_SCALE_UNIFORM
+ : COMMIT_MOUSE_SCALE;
+ }
+
+private:
+
+ static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) {
+ //sp_select_context_get_type();
+ switch (c % 2) {
+ case 0:
+ return Glib::wrap(handles[1], true);
+ break;
+ default:
+ return Glib::wrap(handles[0], true);
+ break;
+ }
+ }
+
+ Geom::Point _sc_center;
+ Geom::Point _sc_opposite;
+ unsigned _corner;
+};
+
+/**
+ * Side scaling handle for node transforms.
+ */
+class ScaleSideHandle : public ScaleHandle {
+public:
+ ScaleSideHandle(TransformHandleSet &th, unsigned side, unsigned d_side)
+ : ScaleHandle(th, side_to_anchor(d_side), _side_to_pixbuf(side))
+ , _side(side)
+ {}
+protected:
+ void startTransform() override {
+ _sc_center = _th.rotationCenter();
+ Geom::Rect b = _th.bounds();
+ _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3));
+ _last_scale_x = _last_scale_y = 1.0;
+ }
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override {
+ Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite;
+ Geom::Point vs;
+ Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2);
+ Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2);
+
+ // avoid exploding the selection
+ if (Geom::are_near(scc[d1], _origin[d1]))
+ return Geom::identity();
+
+ vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1];
+ if (held_alt(*event)) {
+ if (fabs(vs[d1]) >= 1.0) {
+ vs[d1] = round(vs[d1]);
+ } else {
+ vs[d1] = 1.0 / round(1.0 / MIN(vs[d1],10));
+ }
+ vs[d2] = 1.0;
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ bool uniform = held_control(*event);
+ Inkscape::PureStretchConstrained psc = Inkscape::PureStretchConstrained(vs[d1], scc, d1, uniform);
+ m.snapTransformed(_snap_points, _origin, psc);
+ m.unSetup();
+
+ if (psc.best_snapped_point.getSnapped()) {
+ Geom::Point result = psc.getStretchSnapped().vector(); //best_snapped_point.getTransformation();
+ vs[d1] = result[d1];
+ vs[d2] = result[d2];
+ } else {
+ // on ctrl, apply uniform scaling instead of stretching
+ // Preserve aspect ratio, but never flip in the dimension not being edited (by using fabs())
+ vs[d2] = uniform ? fabs(vs[d1]) : 1.0;
+ }
+ }
+
+ _last_scale_x = vs[Geom::X];
+ _last_scale_y = vs[Geom::Y];
+ Geom::Affine t = Geom::Translate(-scc)
+ * Geom::Scale(vs)
+ * Geom::Translate(scc);
+ return t;
+ }
+ CommitEvent getCommitEvent() override {
+ return _last_transform.isUniformScale()
+ ? COMMIT_MOUSE_SCALE_UNIFORM
+ : COMMIT_MOUSE_SCALE;
+ }
+private:
+ static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned c) {
+ //sp_select_context_get_type();
+ switch (c % 2) {
+ case 0: return Glib::wrap(handles[3], true);
+ default: return Glib::wrap(handles[2], true);
+ }
+ }
+ Geom::Point _sc_center;
+ Geom::Point _sc_opposite;
+ unsigned _side;
+};
+
+/**
+ * Rotation handle for node transforms.
+ */
+class RotateHandle : public TransformHandle {
+public:
+ RotateHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner)
+ : TransformHandle(th, corner_to_anchor(d_corner), _corner_to_pixbuf(d_corner))
+ , _corner(corner)
+ {}
+protected:
+
+ void startTransform() override {
+ _rot_center = _th.rotationCenter();
+ _rot_opposite = _th.bounds().corner(_corner + 2);
+ _last_angle = 0;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override
+ {
+ Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center;
+ double angle = Geom::angle_between(_origin - rotc, new_pos - rotc);
+ if (held_control(*event)) {
+ angle = snap_angle(angle);
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+ Inkscape::PureRotateConstrained prc = Inkscape::PureRotateConstrained(angle, rotc);
+ m.snapTransformed(_snap_points, _origin, prc);
+ m.unSetup();
+
+ if (prc.best_snapped_point.getSnapped()) {
+ angle = prc.getAngleSnapped(); //best_snapped_point.getTransformation()[0];
+ }
+ }
+
+ _last_angle = angle;
+ Geom::Affine t = Geom::Translate(-rotc)
+ * Geom::Rotate(angle)
+ * Geom::Translate(rotc);
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override { return COMMIT_MOUSE_ROTATE; }
+
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_shift(state)) {
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: rotate around the opposite corner and snap "
+ "angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: rotate around the opposite corner");
+ }
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Ctrl</b>: snap angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Rotation handle</b>: drag to rotate "
+ "the selection around the rotation center");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ return format_tip(C_("Transform handle tip", "Rotate by %.2f°"),
+ _last_angle * 180.0 / M_PI);
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+private:
+ static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) {
+ //sp_select_context_get_type();
+ switch (c % 4) {
+ case 0: return Glib::wrap(handles[7], true);
+ case 1: return Glib::wrap(handles[6], true);
+ case 2: return Glib::wrap(handles[5], true);
+ default: return Glib::wrap(handles[4], true);
+ }
+ }
+ Geom::Point _rot_center;
+ Geom::Point _rot_opposite;
+ unsigned _corner;
+ static double _last_angle;
+};
+double RotateHandle::_last_angle = 0;
+
+class SkewHandle : public TransformHandle {
+public:
+ SkewHandle(TransformHandleSet &th, unsigned side, unsigned d_side)
+ : TransformHandle(th, side_to_anchor(d_side), _side_to_pixbuf(side))
+ , _side(side)
+ {}
+
+protected:
+
+ void startTransform() override {
+ _skew_center = _th.rotationCenter();
+ Geom::Rect b = _th.bounds();
+ _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3));
+ _last_angle = 0;
+ _last_horizontal = _side % 2;
+ }
+
+ Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override
+ {
+ Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite;
+ Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2);
+ Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2);
+
+ Geom::Point const initial_delta = _origin - scc;
+
+ if (fabs(initial_delta[d1]) < 1e-15) {
+ return Geom::Affine();
+ }
+
+ // Calculate the scale factors, which can be either visual or geometric
+ // depending on which type of bbox is currently being used (see preferences -> selector tool)
+ Geom::Scale scale = calcScaleFactors(_origin, new_pos, scc, false);
+ Geom::Scale skew = calcScaleFactors(_origin, new_pos, scc, true);
+ scale[d2] = 1;
+ skew[d2] = 1;
+
+ // Skew handles allow scaling up to integer multiples of the original size
+ // in the second direction; prevent explosions
+
+ if (fabs(scale[d1]) < 1) {
+ // Prevent shrinking of the selected object, while allowing mirroring
+ scale[d1] = copysign(1.0, scale[d1]);
+ } else {
+ // Allow expanding of the selected object by integer multiples
+ scale[d1] = floor(scale[d1] + 0.5);
+ }
+
+ double angle = atan(skew[d1] / scale[d1]);
+
+ if (held_control(*event)) {
+ angle = snap_angle(angle);
+ skew[d1] = tan(angle) * scale[d1];
+ } else {
+ SnapManager &m = _th._desktop->namedview->snap_manager;
+ m.setupIgnoreSelection(_th._desktop, true, &_unselected_points);
+
+ Inkscape::PureSkewConstrained psc = Inkscape::PureSkewConstrained(skew[d1], scale[d1], scc, d2);
+ m.snapTransformed(_snap_points, _origin, psc);
+ m.unSetup();
+
+ if (psc.best_snapped_point.getSnapped()) {
+ skew[d1] = psc.getSkewSnapped(); //best_snapped_point.getTransformation()[0];
+ }
+ }
+
+ _last_angle = angle;
+
+ // Update the handle position
+ Geom::Point new_new_pos;
+ new_new_pos[d2] = initial_delta[d1] * skew[d1] + _origin[d2];
+ new_new_pos[d1] = initial_delta[d1] * scale[d1] + scc[d1];
+
+ // Calculate the relative affine
+ Geom::Affine relative_affine = Geom::identity();
+ relative_affine[2*d1 + d1] = (new_new_pos[d1] - scc[d1]) / initial_delta[d1];
+ relative_affine[2*d1 + (d2)] = (new_new_pos[d2] - _origin[d2]) / initial_delta[d1];
+ relative_affine[2*(d2) + (d1)] = 0;
+ relative_affine[2*(d2) + (d2)] = 1;
+
+ for (int i = 0; i < 2; i++) {
+ if (fabs(relative_affine[3*i]) < 1e-15) {
+ relative_affine[3*i] = 1e-15;
+ }
+ }
+
+ Geom::Affine t = Geom::Translate(-scc)
+ * relative_affine
+ * Geom::Translate(scc);
+
+ return t;
+ }
+
+ CommitEvent getCommitEvent() override {
+ return (_side % 2)
+ ? COMMIT_MOUSE_SKEW_Y
+ : COMMIT_MOUSE_SKEW_X;
+ }
+
+ Glib::ustring _getTip(unsigned state) const override {
+ if (state_held_shift(state)) {
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Shift+Ctrl</b>: skew about the rotation center with snapping "
+ "to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip", "<b>Shift</b>: skew about the rotation center");
+ }
+ if (state_held_control(state)) {
+ return format_tip(C_("Transform handle tip",
+ "<b>Ctrl</b>: snap skew angle to %f° increments"), snap_increment_degrees());
+ }
+ return C_("Transform handle tip",
+ "<b>Skew handle</b>: drag to skew (shear) selection about "
+ "the opposite handle");
+ }
+
+ Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override {
+ if (_last_horizontal) {
+ return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"),
+ _last_angle * 360.0);
+ } else {
+ return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"),
+ _last_angle * 360.0);
+ }
+ }
+
+ bool _hasDragTips() const override { return true; }
+
+private:
+
+ static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned s) {
+ //sp_select_context_get_type();
+ switch (s % 4) {
+ case 0: return Glib::wrap(handles[10], true);
+ case 1: return Glib::wrap(handles[9], true);
+ case 2: return Glib::wrap(handles[8], true);
+ default: return Glib::wrap(handles[11], true);
+ }
+ }
+ Geom::Point _skew_center;
+ Geom::Point _skew_opposite;
+ unsigned _side;
+ static bool _last_horizontal;
+ static double _last_angle;
+};
+bool SkewHandle::_last_horizontal = false;
+double SkewHandle::_last_angle = 0;
+
+class RotationCenter : public ControlPoint {
+
+public:
+ RotationCenter(TransformHandleSet &th) :
+ ControlPoint(th._desktop, Geom::Point(), SP_ANCHOR_CENTER,
+ _get_pixbuf(),
+ _center_cset, th._transform_handle_group),
+ _th(th)
+ {
+ setVisible(false);
+ }
+
+protected:
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override {
+ SnapManager &sm = _th._desktop->namedview->snap_manager;
+ sm.setup(_th._desktop);
+ bool snap = !held_shift(*event) && sm.someSnapperMightSnap();
+ if (held_control(*event)) {
+ // constrain to axes
+ Geom::Point origin = _last_drag_origin();
+ std::vector<Inkscape::Snapper::SnapConstraint> constraints;
+ constraints.emplace_back(origin, Geom::Point(1, 0));
+ constraints.emplace_back(origin, Geom::Point(0, 1));
+ new_pos = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos,
+ SNAPSOURCE_ROTATION_CENTER), constraints, held_shift(*event)).getPoint();
+ } else if (snap) {
+ sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_ROTATION_CENTER);
+ }
+ sm.unSetup();
+ }
+ Glib::ustring _getTip(unsigned /*state*/) const override {
+ return C_("Transform handle tip",
+ "<b>Rotation center</b>: drag to change the origin of transforms");
+ }
+
+private:
+
+ static Glib::RefPtr<Gdk::Pixbuf> _get_pixbuf() {
+ //sp_select_context_get_type();
+ return Glib::wrap(handles[12], true);
+ }
+
+ static ColorSet _center_cset;
+ TransformHandleSet &_th;
+};
+
+ControlPoint::ColorSet RotationCenter::_center_cset = {
+ {0x00000000, 0x000000ff},
+ {0x00000000, 0xff0000b0},
+ {0x00000000, 0xff0000b0},
+ //
+ {0x00000000, 0x000000ff},
+ {0x00000000, 0xff0000b0},
+ {0x00000000, 0xff0000b0}
+};
+
+
+TransformHandleSet::TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group)
+ : Manipulator(d)
+ , _active(nullptr)
+ , _transform_handle_group(th_group)
+ , _mode(MODE_SCALE)
+ , _in_transform(false)
+ , _visible(true)
+{
+ _trans_outline = static_cast<CtrlRect*>(sp_canvas_item_new(_desktop->getControls(),
+ SP_TYPE_CTRLRECT, nullptr));
+ sp_canvas_item_hide(_trans_outline);
+ _trans_outline->setDashed(true);
+
+ bool y_inverted = !d->is_yaxisdown();
+ for (unsigned i = 0; i < 4; ++i) {
+ unsigned d_c = y_inverted ? i : 3 - i;
+ unsigned d_s = y_inverted ? i : 6 - i;
+ _scale_corners[i] = new ScaleCornerHandle(*this, i, d_c);
+ _scale_sides[i] = new ScaleSideHandle(*this, i, d_s);
+ _rot_corners[i] = new RotateHandle(*this, i, d_c);
+ _skew_sides[i] = new SkewHandle(*this, i, d_s);
+ }
+ _center = new RotationCenter(*this);
+ // when transforming, update rotation center position
+ signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform));
+}
+
+TransformHandleSet::~TransformHandleSet()
+{
+ for (auto & _handle : _handles) {
+ delete _handle;
+ }
+}
+
+void TransformHandleSet::setMode(Mode m)
+{
+ _mode = m;
+ _updateVisibility(_visible);
+}
+
+Geom::Rect TransformHandleSet::bounds() const
+{
+ return Geom::Rect(*_scale_corners[0], *_scale_corners[2]);
+}
+
+ControlPoint const &TransformHandleSet::rotationCenter() const
+{
+ return *_center;
+}
+
+ControlPoint &TransformHandleSet::rotationCenter()
+{
+ return *_center;
+}
+
+void TransformHandleSet::setVisible(bool v)
+{
+ if (_visible != v) {
+ _visible = v;
+ _updateVisibility(_visible);
+ }
+}
+
+void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center)
+{
+ if (_in_transform) {
+ _trans_outline->setRectangle(r);
+ } else {
+ for (unsigned i = 0; i < 4; ++i) {
+ _scale_corners[i]->move(r.corner(i));
+ _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1)));
+ _rot_corners[i]->move(r.corner(i));
+ _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1)));
+ }
+ if (!preserve_center) _center->move(r.midpoint());
+ if (_visible) _updateVisibility(true);
+ }
+}
+
+bool TransformHandleSet::event(Inkscape::UI::Tools::ToolBase *, GdkEvent*)
+{
+ return false;
+}
+
+void TransformHandleSet::_emitTransform(Geom::Affine const &t)
+{
+ signal_transform.emit(t);
+ _center->transform(t);
+}
+
+void TransformHandleSet::_setActiveHandle(ControlPoint *th)
+{
+ _active = th;
+ if (_in_transform)
+ throw std::logic_error("Transform initiated when another transform in progress");
+ _in_transform = true;
+ // hide all handles except the active one
+ _updateVisibility(false);
+ sp_canvas_item_show(_trans_outline);
+}
+
+void TransformHandleSet::_clearActiveHandle()
+{
+ // This can only be called from handles, so they had to be visible before _setActiveHandle
+ sp_canvas_item_hide(_trans_outline);
+ _active = nullptr;
+ _in_transform = false;
+ _updateVisibility(_visible);
+}
+
+void TransformHandleSet::_updateVisibility(bool v)
+{
+ if (v) {
+ Geom::Rect b = bounds();
+ Geom::Point handle_size(
+ gdk_pixbuf_get_width(handles[0]) / _desktop->current_zoom(),
+ gdk_pixbuf_get_height(handles[0]) / _desktop->current_zoom());
+ Geom::Point bp = b.dimensions();
+
+ // do not scale when the bounding rectangle has zero width or height
+ bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0);
+ // do not rotate if the bounding rectangle is degenerate
+ bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0);
+ bool show_scale_side[2], show_skew[2];
+
+ // show sides if:
+ // a) there is enough space between corner handles, or
+ // b) corner handles are not shown, but side handles make sense
+ // this affects horizontal and vertical scale handles; skew handles never
+ // make sense if rotate handles are not shown
+ for (unsigned i = 0; i < 2; ++i) {
+ Geom::Dim2 d = static_cast<Geom::Dim2>(i);
+ Geom::Dim2 otherd = static_cast<Geom::Dim2>((i+1)%2);
+ show_scale_side[i] = (_mode == MODE_SCALE);
+ show_scale_side[i] &= (show_scale ? bp[d] >= handle_size[d]
+ : !Geom::are_near(bp[otherd], 0));
+ show_skew[i] = (show_rotate && bp[d] >= handle_size[d]
+ && !Geom::are_near(bp[otherd], 0));
+ }
+ for (unsigned i = 0; i < 4; ++i) {
+ _scale_corners[i]->setVisible(show_scale);
+ _rot_corners[i]->setVisible(show_rotate);
+ _scale_sides[i]->setVisible(show_scale_side[i%2]);
+ _skew_sides[i]->setVisible(show_skew[i%2]);
+ }
+ // show rotation center if there is enough space (?)
+ _center->setVisible(show_rotate /*&& bp[Geom::X] > handle_size[Geom::X]
+ && bp[Geom::Y] > handle_size[Geom::Y]*/);
+ } else {
+ for (auto & _handle : _handles) {
+ if (_handle != _active)
+ _handle->setVisible(false);
+ }
+ }
+
+}
+
+} // 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/tool/transform-handle-set.h b/src/ui/tool/transform-handle-set.h
new file mode 100644
index 0000000..803e27c
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.h
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Affine transform handles component
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H
+#define SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H
+
+#include <memory>
+#include <gdk/gdk.h>
+#include <2geom/forward.h>
+#include "ui/tool/commit-events.h"
+#include "ui/tool/manipulator.h"
+#include "enums.h"
+
+class SPDesktop;
+class CtrlRect;
+namespace Inkscape {
+namespace UI {
+
+class RotateHandle;
+class SkewHandle;
+class ScaleCornerHandle;
+class ScaleSideHandle;
+class RotationCenter;
+
+class TransformHandleSet : public Manipulator {
+public:
+
+ enum Mode {
+ MODE_SCALE,
+ MODE_ROTATE_SKEW
+ };
+
+ TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group);
+ ~TransformHandleSet() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+
+ bool visible() const { return _visible; }
+ Mode mode() const { return _mode; }
+ Geom::Rect bounds() const;
+ void setVisible(bool v);
+
+ /** Sets the mode of transform handles (scale or rotate). */
+ void setMode(Mode m);
+
+ void setBounds(Geom::Rect const &, bool preserve_center = false);
+
+ bool transforming() { return _in_transform; }
+
+ ControlPoint const &rotationCenter() const;
+ ControlPoint &rotationCenter();
+
+ sigc::signal<void, Geom::Affine const &> signal_transform;
+ sigc::signal<void, CommitEvent> signal_commit;
+
+private:
+
+ void _emitTransform(Geom::Affine const &);
+ void _setActiveHandle(ControlPoint *h);
+ void _clearActiveHandle();
+
+ /** Update the visibility of transformation handles according to settings and the dimensions
+ * of the bounding box. It hides the handles that would have no effect or lead to
+ * discontinuities. Additionally, side handles for which there is no space are not shown.
+ */
+ void _updateVisibility(bool v);
+
+ // TODO unions must GO AWAY:
+ union {
+ ControlPoint *_handles[17];
+ struct {
+ ScaleCornerHandle *_scale_corners[4];
+ ScaleSideHandle *_scale_sides[4];
+ RotateHandle *_rot_corners[4];
+ SkewHandle *_skew_sides[4];
+ RotationCenter *_center;
+ };
+ };
+
+ ControlPoint *_active;
+ SPCanvasGroup *_transform_handle_group;
+ CtrlRect *_trans_outline;
+ Mode _mode;
+ bool _in_transform;
+ bool _visible;
+ bool _rot_center_visible;
+ friend class TransformHandle;
+ friend class RotationCenter;
+};
+
+/** Base class for node transform handles to simplify implementation. */
+class TransformHandle : public ControlPoint
+{
+public:
+ TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb);
+ void getNextClosestPoint(bool reverse);
+
+protected:
+ virtual void startTransform() {}
+ virtual void endTransform() {}
+ virtual Geom::Affine computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0;
+ virtual CommitEvent getCommitEvent() = 0;
+
+ Geom::Affine _last_transform;
+ Geom::Point _origin;
+ TransformHandleSet &_th;
+ std::vector<Inkscape::SnapCandidatePoint> _snap_points;
+ std::vector<Inkscape::SnapCandidatePoint> _unselected_points;
+ std::vector<Inkscape::SnapCandidatePoint> _all_snap_sources_sorted;
+ std::vector<Inkscape::SnapCandidatePoint>::iterator _all_snap_sources_iter;
+
+private:
+ bool grabbed(GdkEventMotion *) override;
+ void dragged(Geom::Point &new_pos, GdkEventMotion *event) override;
+ void ungrabbed(GdkEventButton *) override;
+
+ static ColorSet thandle_cset;
+};
+
+} // 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/toolbar/arc-toolbar.cpp b/src/ui/toolbar/arc-toolbar.cpp
new file mode 100644
index 0000000..6c17ab1
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.cpp
@@ -0,0 +1,561 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Arc aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "arc-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "mod360.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "object/sp-ellipse.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/pref-pusher.h"
+#include "ui/tools/arc-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+#include "widgets/spinbutton-events.h"
+#include "widgets/widget-sizes.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+
+static Inkscape::XML::NodeEventVector arc_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::ArcToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ArcToolbar::ArcToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _freeze(false),
+ _repr(nullptr)
+{
+ _tracker->setActiveUnit(desktop->getNamedView()->display_units);
+ auto prefs = Inkscape::Preferences::get();
+
+ {
+ _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")));
+ _mode_item->set_use_markup(true);
+ add(*_mode_item);
+ }
+
+ /* Radius X */
+ {
+ std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ auto rx_val = prefs->getDouble("/tools/shapes/arc/rx", 0);
+ _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-rx", _("Rx:"), _rx_adj));
+ _rx_item->set_tooltip_text(_("Horizontal radius of the circle, ellipse, or arc"));
+ _rx_item->set_custom_numeric_menu_data(values);
+ _tracker->addAdjustment(_rx_adj->gobj());
+ _rx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed),
+ _rx_adj, "rx"));
+ _rx_item->set_sensitive(false);
+ add(*_rx_item);
+ }
+
+ /* Radius Y */
+ {
+ std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ auto ry_val = prefs->getDouble("/tools/shapes/arc/ry", 0);
+ _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-ry", _("Ry:"), _ry_adj));
+ _ry_item->set_tooltip_text(_("Vertical radius of the circle, ellipse, or arc"));
+ _ry_item->set_custom_numeric_menu_data(values);
+ _tracker->addAdjustment(_ry_adj->gobj());
+ _ry_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed),
+ _ry_adj, "ry"));
+ _ry_item->set_sensitive(false);
+ add(*_ry_item);
+ }
+
+ // add the units menu
+ {
+ auto unit_menu = _tracker->create_tool_item(_("Units"), ("") );
+ add(*unit_menu);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Start */
+ {
+ auto start_val = prefs->getDouble("/tools/shapes/arc/start", 0.0);
+ _start_adj = Gtk::Adjustment::create(start_val, -360.0, 360.0, 1.0, 10.0);
+ auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-start", _("Start:"), _start_adj));
+ eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's start point"));
+ eact->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ add(*eact);
+ }
+
+ /* End */
+ {
+ auto end_val = prefs->getDouble("/tools/shapes/arc/end", 0.0);
+ _end_adj = Gtk::Adjustment::create(end_val, -360.0, 360.0, 1.0, 10.0);
+ auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-end", _("End:"), _end_adj));
+ eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's end point"));
+ eact->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ add(*eact);
+ }
+ _start_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed),
+ _start_adj, "start", _end_adj));
+ _end_adj->signal_value_changed().connect( sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed),
+ _end_adj, "end", _start_adj));
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Arc: Slice, Arc, Chord */
+ {
+ Gtk::RadioToolButton::Group type_group;
+
+ auto slice_btn = Gtk::manage(new Gtk::RadioToolButton(_("Slice")));
+ slice_btn->set_tooltip_text(_("Switch to slice (closed shape with two radii)"));
+ slice_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-segment"));
+ _type_buttons.push_back(slice_btn);
+
+ auto arc_btn = Gtk::manage(new Gtk::RadioToolButton(_("Arc (Open)")));
+ arc_btn->set_tooltip_text(_("Switch to arc (unclosed shape)"));
+ arc_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-arc"));
+ _type_buttons.push_back(arc_btn);
+
+ auto chord_btn = Gtk::manage(new Gtk::RadioToolButton(_("Chord")));
+ chord_btn->set_tooltip_text(_("Switch to chord (closed shape)"));
+ chord_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-chord"));
+ _type_buttons.push_back(chord_btn);
+
+ slice_btn->set_group(type_group);
+ arc_btn->set_group(type_group);
+ chord_btn->set_group(type_group);
+
+ gint type = prefs->getInt("/tools/shapes/arc/arc_type", 0);
+ _type_buttons[type]->set_active();
+
+ int btn_index = 0;
+ for (auto btn : _type_buttons)
+ {
+ btn->set_sensitive();
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::type_changed), btn_index++));
+ add(*btn);
+ }
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Make Whole */
+ {
+ _make_whole = Gtk::manage(new Gtk::ToolButton(_("Make whole")));
+ _make_whole->set_tooltip_text(_("Make the shape a whole ellipse, not arc or segment"));
+ _make_whole->set_icon_name(INKSCAPE_ICON("draw-ellipse-whole"));
+ _make_whole->signal_clicked().connect(sigc::mem_fun(*this, &ArcToolbar::defaults));
+ add(*_make_whole);
+ _make_whole->set_sensitive(true);
+ }
+
+ _single = true;
+ // sensitivize make whole and open checkbox
+ {
+ sensitivize( _start_adj->get_value(), _end_adj->get_value() );
+ }
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &ArcToolbar::check_ec));
+
+ show_all();
+}
+
+ArcToolbar::~ArcToolbar()
+{
+ if(_repr) {
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+GtkWidget *
+ArcToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new ArcToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+ArcToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name)
+{
+ // Per SVG spec "a [radius] value of zero disables rendering of the element".
+ // However our implementation does not allow a setting of zero in the UI (not even in the XML editor)
+ // and ugly things happen if it's forced here, so better leave the properties untouched.
+ if (!adj->get_value()) {
+ return;
+ }
+
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ SPDocument* document = _desktop->getDocument();
+ Geom::Scale scale = document->getDocumentScale();
+
+ if (DocumentUndo::getUndoSensitive(document)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name,
+ Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze || _tracker->isUpdating()) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_GENERICELLIPSE(item)) {
+
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(item);
+
+ if (!strcmp(value_name, "rx")) {
+ ge->setVisibleRx(Quantity::convert(adj->get_value(), unit, "px"));
+ } else {
+ ge->setVisibleRy(Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ ge->normalize();
+ (SP_OBJECT(ge))->updateRepr();
+ (SP_OBJECT(ge))->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_ARC,
+ _("Ellipse: Change radius"));
+ }
+
+ _freeze = false;
+}
+
+void
+ArcToolbar::startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ Glib::RefPtr<Gtk::Adjustment>& other_adj)
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name, adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ gchar* namespaced_name = g_strconcat("sodipodi:", value_name, NULL);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_GENERICELLIPSE(item)) {
+
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(item);
+
+ if (!strcmp(value_name, "start")) {
+ ge->start = (adj->get_value() * M_PI)/ 180;
+ } else {
+ ge->end = (adj->get_value() * M_PI)/ 180;
+ }
+
+ ge->normalize();
+ (SP_OBJECT(ge))->updateRepr();
+ (SP_OBJECT(ge))->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+
+ modmade = true;
+ }
+ }
+
+ g_free(namespaced_name);
+
+ sensitivize( adj->get_value(), other_adj->get_value() );
+
+ if (modmade) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), value_name, SP_VERB_CONTEXT_ARC,
+ _("Arc: Change start/end"));
+ }
+
+ _freeze = false;
+}
+
+void
+ArcToolbar::type_changed( int type )
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/shapes/arc/arc_type", type);
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ Glib::ustring arc_type = "slice";
+ bool open = false;
+ switch (type) {
+ case 0:
+ arc_type = "slice";
+ open = false;
+ break;
+ case 1:
+ arc_type = "arc";
+ open = true;
+ break;
+ case 2:
+ arc_type = "chord";
+ open = true; // For backward compat, not truly open but chord most like arc.
+ break;
+ default:
+ std::cerr << "sp_arctb_type_changed: bad arc type: " << type << std::endl;
+ }
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_GENERICELLIPSE(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttribute("sodipodi:open", (open?"true":nullptr) );
+ repr->setAttribute("sodipodi:arc-type", arc_type);
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_ARC,
+ _("Arc: Changed arc type"));
+ }
+
+ _freeze = false;
+}
+
+void
+ArcToolbar::defaults()
+{
+ _start_adj->set_value(0.0);
+ _end_adj->set_value(0.0);
+
+ if(_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(_desktop->canvas));
+}
+
+void
+ArcToolbar::sensitivize( double v1, double v2 )
+{
+ if (v1 == 0 && v2 == 0) {
+ if (_single) { // only for a single selected ellipse (for now)
+ for (auto btn : _type_buttons) btn->set_sensitive(false);
+ _make_whole->set_sensitive(false);
+ }
+ } else {
+ for (auto btn : _type_buttons) btn->set_sensitive(true);
+ _make_whole->set_sensitive(true);
+ }
+}
+
+void
+ArcToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (SP_IS_ARC_CONTEXT(ec)) {
+ _changed = _desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ArcToolbar::selection_changed));
+ selection_changed(desktop->getSelection());
+ } else {
+ if (_changed) {
+ _changed.disconnect();
+ if(_repr) {
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+ }
+}
+
+void
+ArcToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+
+ if ( _repr ) {
+ _item = nullptr;
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = nullptr;
+
+ for(auto i : selection->items()){
+ if (SP_IS_GENERICELLIPSE(i)) {
+ n_selected++;
+ item = i;
+ repr = item->getRepr();
+ }
+ }
+
+ _single = false;
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _single = true;
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ _rx_item->set_sensitive(true);
+ _ry_item->set_sensitive(true);
+
+ if (repr) {
+ _repr = repr;
+ _item = item;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&arc_tb_repr_events, this);
+ _repr->synthesizeEvents(&arc_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ sensitivize( 1, 0 );
+ }
+}
+
+void
+ArcToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const * /*name*/,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer data)
+{
+ auto toolbar = reinterpret_cast<ArcToolbar *>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ if (toolbar->_item && SP_IS_GENERICELLIPSE(toolbar->_item)) {
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(toolbar->_item);
+
+ Unit const *unit = toolbar->_tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ gdouble rx = ge->getVisibleRx();
+ gdouble ry = ge->getVisibleRy();
+ toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit));
+ toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit));
+ }
+
+ gdouble start = 0.;
+ gdouble end = 0.;
+ sp_repr_get_double(repr, "sodipodi:start", &start);
+ sp_repr_get_double(repr, "sodipodi:end", &end);
+
+ toolbar->_start_adj->set_value(mod360((start * 180)/M_PI));
+ toolbar->_end_adj->set_value(mod360((end * 180)/M_PI));
+
+ toolbar->sensitivize(toolbar->_start_adj->get_value(), toolbar->_end_adj->get_value());
+
+ char const *arctypestr = nullptr;
+ arctypestr = repr->attribute("sodipodi:arc-type");
+ if (!arctypestr) { // For old files.
+ char const *openstr = nullptr;
+ openstr = repr->attribute("sodipodi:open");
+ arctypestr = (openstr ? "arc" : "slice");
+ }
+
+ if (!strcmp(arctypestr,"slice")) {
+ toolbar->_type_buttons[0]->set_active();
+ } else if (!strcmp(arctypestr,"arc")) {
+ toolbar->_type_buttons[1]->set_active();
+ } else {
+ toolbar->_type_buttons[2]->set_active();
+ }
+
+ toolbar->_freeze = 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/toolbar/arc-toolbar.h b/src/ui/toolbar/arc-toolbar.h
new file mode 100644
index 0000000..b0b0450
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_ARC_TOOLBAR_H
+#define SEEN_ARC_TOOLBAR_H
+
+/**
+ * @file
+ * 3d box aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPItem;
+
+namespace Gtk {
+class RadioToolButton;
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class ArcToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+
+ UI::Widget::SpinButtonToolItem *_rx_item;
+ UI::Widget::SpinButtonToolItem *_ry_item;
+
+ UI::Widget::LabelToolItem *_mode_item;
+
+ std::vector<Gtk::RadioToolButton *> _type_buttons;
+ Gtk::ToolButton *_make_whole;
+
+ Glib::RefPtr<Gtk::Adjustment> _rx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _ry_adj;
+ Glib::RefPtr<Gtk::Adjustment> _start_adj;
+ Glib::RefPtr<Gtk::Adjustment> _end_adj;
+
+ bool _freeze;
+ bool _single;
+
+ XML::Node *_repr;
+ SPItem *_item;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name);
+ void startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ Glib::RefPtr<Gtk::Adjustment>& other_adj);
+ void type_changed( int type );
+ void defaults();
+ void sensitivize( double v1, double v2 );
+ void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+ sigc::connection _changed;
+
+protected:
+ ArcToolbar(SPDesktop *desktop);
+ ~ArcToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_ARC_TOOLBAR_H */
diff --git a/src/ui/toolbar/box3d-toolbar.cpp b/src/ui/toolbar/box3d-toolbar.cpp
new file mode 100644
index 0000000..8f14277
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.cpp
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * 3d box aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "box3d-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "object/box3d.h"
+#include "object/persp3d.h"
+
+#include "ui/icon-names.h"
+#include "ui/pref-pusher.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector box3d_persp_tb_repr_events =
+{
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::Box3DToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+Box3DToolbar::Box3DToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _repr(nullptr),
+ _freeze(false)
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto document = desktop->getDocument();
+ auto persp_impl = document->getCurrentPersp3DImpl();
+
+ /* Angle X */
+ {
+ std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90};
+ auto angle_x_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_x", 30);
+ _angle_x_adj = Gtk::Adjustment::create(angle_x_val, -360.0, 360.0, 1.0, 10.0);
+ _angle_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-x", _("Angle X:"), _angle_x_adj));
+ // TRANSLATORS: PL is short for 'perspective line'
+ _angle_x_item->set_tooltip_text(_("Angle of PLs in X direction"));
+ _angle_x_item->set_custom_numeric_menu_data(values);
+ _angle_x_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _angle_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed),
+ _angle_x_adj, Proj::X));
+ add(*_angle_x_item);
+ }
+
+ if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::X)) {
+ _angle_x_item->set_sensitive(true);
+ } else {
+ _angle_x_item->set_sensitive(false);
+ }
+
+ /* VP X state */
+ {
+ // TRANSLATORS: VP is short for 'vanishing point'
+ _vp_x_state_item = add_toggle_button(_("State of VP in X direction"),
+ _("Toggle VP in X direction between 'finite' and 'infinite' (=parallel)"));
+ _vp_x_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel"));
+ _vp_x_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::X));
+ _angle_x_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) );
+ _vp_x_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) );
+ }
+
+ /* Angle Y */
+ {
+ auto angle_y_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_y", 30);
+ _angle_y_adj = Gtk::Adjustment::create(angle_y_val, -360.0, 360.0, 1.0, 10.0);
+ _angle_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-y", _("Angle Y:"), _angle_y_adj));
+ // TRANSLATORS: PL is short for 'perspective line'
+ _angle_y_item->set_tooltip_text(_("Angle of PLs in Y direction"));
+ std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90};
+ _angle_y_item->set_custom_numeric_menu_data(values);
+ _angle_y_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _angle_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed),
+ _angle_y_adj, Proj::Y));
+ add(*_angle_y_item);
+ }
+
+ if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::Y)) {
+ _angle_y_item->set_sensitive(true);
+ } else {
+ _angle_y_item->set_sensitive(false);
+ }
+
+ /* VP Y state */
+ {
+ // TRANSLATORS: VP is short for 'vanishing point'
+ _vp_y_state_item = add_toggle_button(_("State of VP in Y direction"),
+ _("Toggle VP in Y direction between 'finite' and 'infinite' (=parallel)"));
+ _vp_y_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel"));
+ _vp_y_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Y));
+ _angle_y_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) );
+ _vp_y_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) );
+ }
+
+ /* Angle Z */
+ {
+ auto angle_z_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_z", 30);
+ _angle_z_adj = Gtk::Adjustment::create(angle_z_val, -360.0, 360.0, 1.0, 10.0);
+ _angle_z_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-z", _("Angle Z:"), _angle_z_adj));
+ // TRANSLATORS: PL is short for 'perspective line'
+ _angle_z_item->set_tooltip_text(_("Angle of PLs in Z direction"));
+ std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90};
+ _angle_z_item->set_custom_numeric_menu_data(values);
+ _angle_z_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _angle_z_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed),
+ _angle_z_adj, Proj::Z));
+ add(*_angle_z_item);
+ }
+
+ if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::Z)) {
+ _angle_z_item->set_sensitive(true);
+ } else {
+ _angle_z_item->set_sensitive(false);
+ }
+
+ /* VP Z state */
+ {
+ // TRANSLATORS: VP is short for 'vanishing point'
+ _vp_z_state_item = add_toggle_button(_("State of VP in Z direction"),
+ _("Toggle VP in Z direction between 'finite' and 'infinite' (=parallel)"));
+ _vp_z_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel"));
+ _vp_z_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Z));
+ _angle_z_item->set_sensitive(!prefs->getBool("/tools/shapes/3dbox/vp_z_state", true));
+ _vp_z_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_z_state", true) );
+ }
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &Box3DToolbar::check_ec));
+
+ show_all();
+}
+
+GtkWidget *
+Box3DToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new Box3DToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+Box3DToolbar::angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Proj::Axis axis)
+{
+ SPDocument *document = _desktop->getDocument();
+
+ // quit if run by the attr_changed or selection changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ std::list<Persp3D *> sel_persps = _desktop->getSelection()->perspList();
+ if (sel_persps.empty()) {
+ // this can happen when the document is created; we silently ignore it
+ return;
+ }
+ Persp3D *persp = sel_persps.front();
+
+ persp->perspective_impl->tmat.set_infinite_direction (axis,
+ adj->get_value());
+ persp->updateRepr();
+
+ // TODO: use the correct axis here, too
+ DocumentUndo::maybeDone(document, "perspangle", SP_VERB_CONTEXT_3DBOX, _("3D Box: Change perspective (angle of infinite axis)"));
+
+ _freeze = false;
+}
+
+void
+Box3DToolbar::vp_state_changed(Proj::Axis axis)
+{
+ // TODO: Take all selected perspectives into account
+ auto sel_persps = SP_ACTIVE_DESKTOP->getSelection()->perspList();
+ if (sel_persps.empty()) {
+ // this can happen when the document is created; we silently ignore it
+ return;
+ }
+ Persp3D *persp = sel_persps.front();
+
+ Gtk::ToggleToolButton *btn = nullptr;
+
+ switch(axis) {
+ case Proj::X:
+ btn = _vp_x_state_item;
+ break;
+ case Proj::Y:
+ btn = _vp_y_state_item;
+ break;
+ case Proj::Z:
+ btn = _vp_z_state_item;
+ break;
+ default:
+ return;
+ }
+
+ bool set_infinite = btn->get_active();
+ persp3d_set_VP_state (persp, axis, set_infinite ? Proj::VP_INFINITE : Proj::VP_FINITE);
+}
+
+void
+Box3DToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (SP_IS_BOX3D_CONTEXT(ec)) {
+ _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &Box3DToolbar::selection_changed));
+ selection_changed(desktop->getSelection());
+ } else {
+ if (_changed)
+ _changed.disconnect();
+ }
+}
+
+Box3DToolbar::~Box3DToolbar()
+{
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+/**
+ * \param selection Should not be NULL.
+ */
+// FIXME: This should rather be put into persp3d-reference.cpp or something similar so that it reacts upon each
+// Change of the perspective, and not of the current selection (but how to refer to the toolbar then?)
+void
+Box3DToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ // Here the following should be done: If all selected boxes have finite VPs in a certain direction,
+ // disable the angle entry fields for this direction (otherwise entering a value in them should only
+ // update the perspectives with infinite VPs and leave the other ones untouched).
+
+ Inkscape::XML::Node *persp_repr = nullptr;
+
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = selection->singleItem();
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ // FIXME: Also deal with multiple selected boxes
+ Persp3D *persp = box3d_get_perspective(box);
+ persp_repr = persp->getRepr();
+ if (persp_repr) {
+ _repr = persp_repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&box3d_persp_tb_repr_events, this);
+ _repr->synthesizeEvents(&box3d_persp_tb_repr_events, this);
+
+ SP_ACTIVE_DOCUMENT->setCurrentPersp3D(persp3d_get_from_repr(_repr));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/tools/shapes/3dbox/persp", _repr->attribute("id"));
+
+ _freeze = true;
+ resync_toolbar(_repr);
+ _freeze = false;
+ }
+ }
+}
+
+void
+Box3DToolbar::resync_toolbar(Inkscape::XML::Node *persp_repr)
+{
+ if (!persp_repr) {
+ g_print ("No perspective given to box3d_resync_toolbar().\n");
+ return;
+ }
+
+ Persp3D *persp = persp3d_get_from_repr(persp_repr);
+ if (!persp) {
+ // Hmm, is it an error if this happens?
+ return;
+ }
+ set_button_and_adjustment(persp, Proj::X,
+ _angle_x_adj,
+ _angle_x_item,
+ _vp_x_state_item);
+ set_button_and_adjustment(persp, Proj::Y,
+ _angle_y_adj,
+ _angle_y_item,
+ _vp_y_state_item);
+ set_button_and_adjustment(persp, Proj::Z,
+ _angle_z_adj,
+ _angle_z_item,
+ _vp_z_state_item);
+}
+
+void
+Box3DToolbar::set_button_and_adjustment(Persp3D *persp,
+ Proj::Axis axis,
+ Glib::RefPtr<Gtk::Adjustment>& adj,
+ UI::Widget::SpinButtonToolItem *spin_btn,
+ Gtk::ToggleToolButton *toggle_btn)
+{
+ // TODO: Take all selected perspectives into account but don't touch the state button if not all of them
+ // have the same state (otherwise a call to box3d_vp_z_state_changed() is triggered and the states
+ // are reset).
+ bool is_infinite = !persp3d_VP_is_finite(persp->perspective_impl, axis);
+
+ if (is_infinite) {
+ toggle_btn->set_active(true);
+ spin_btn->set_sensitive(true);
+
+ double angle = persp3d_get_infinite_angle(persp, axis);
+ if (angle != Geom::infinity()) { // FIXME: We should catch this error earlier (don't show the spinbutton at all)
+ adj->set_value(normalize_angle(angle));
+ }
+ } else {
+ toggle_btn->set_active(false);
+ spin_btn->set_sensitive(false);
+ }
+}
+
+void
+Box3DToolbar::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const * /*name*/,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<Box3DToolbar*>(data);
+
+ // quit if run by the attr_changed or selection changed listener
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // set freeze so that it can be caught in box3d_angle_z_value_changed() (to avoid calling
+ // SPDocumentUndo::maybeDone() when the document is undo insensitive)
+ toolbar->_freeze = true;
+
+ // TODO: Only update the appropriate part of the toolbar
+// if (!strcmp(name, "inkscape:vp_z")) {
+ toolbar->resync_toolbar(repr);
+// }
+
+ Persp3D *persp = persp3d_get_from_repr(repr);
+ persp3d_update_box_reprs(persp);
+
+ toolbar->_freeze = false;
+}
+
+/**
+ * \brief normalize angle so that it lies in the interval [0,360]
+ *
+ * TODO: Isn't there something in 2Geom or cmath that does this?
+ */
+double
+Box3DToolbar::normalize_angle(double a) {
+ double angle = a + ((int) (a/360.0))*360;
+ if (angle < 0) {
+ angle += 360.0;
+ }
+ return angle;
+}
+
+}
+}
+}
+
+
+/*
+ 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/toolbar/box3d-toolbar.h b/src/ui/toolbar/box3d-toolbar.h
new file mode 100644
index 0000000..dc74664
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_BOX3D_TOOLBAR_H
+#define SEEN_BOX3D_TOOLBAR_H
+
+/**
+ * @file
+ * 3d box aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+#include "axis-manip.h"
+
+class Persp3D;
+class SPDesktop;
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+class SpinButtonToolItem;
+}
+
+namespace Tools {
+class ToolBase;
+}
+
+namespace Toolbar {
+class Box3DToolbar : public Toolbar {
+private:
+ UI::Widget::SpinButtonToolItem *_angle_x_item;
+ UI::Widget::SpinButtonToolItem *_angle_y_item;
+ UI::Widget::SpinButtonToolItem *_angle_z_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _angle_x_adj;
+ Glib::RefPtr<Gtk::Adjustment> _angle_y_adj;
+ Glib::RefPtr<Gtk::Adjustment> _angle_z_adj;
+
+ Gtk::ToggleToolButton *_vp_x_state_item;
+ Gtk::ToggleToolButton *_vp_y_state_item;
+ Gtk::ToggleToolButton *_vp_z_state_item;
+
+ XML::Node *_repr;
+ bool _freeze;
+
+ void angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Proj::Axis axis);
+ void vp_state_changed(Proj::Axis axis);
+ void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+ void resync_toolbar(Inkscape::XML::Node *persp_repr);
+ void set_button_and_adjustment(Persp3D *persp,
+ Proj::Axis axis,
+ Glib::RefPtr<Gtk::Adjustment>& adj,
+ UI::Widget::SpinButtonToolItem *spin_btn,
+ Gtk::ToggleToolButton *toggle_btn);
+ double normalize_angle(double a);
+
+ sigc::connection _changed;
+
+protected:
+ Box3DToolbar(SPDesktop *desktop);
+ ~Box3DToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+
+};
+}
+}
+}
+#endif /* !SEEN_BOX3D_TOOLBAR_H */
diff --git a/src/ui/toolbar/calligraphy-toolbar.cpp b/src/ui/toolbar/calligraphy-toolbar.cpp
new file mode 100644
index 0000000..34e7a89
--- /dev/null
+++ b/src/ui/toolbar/calligraphy-toolbar.cpp
@@ -0,0 +1,599 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Calligraphy aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "calligraphy-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+#include "ui/uxmanager.h"
+#include "ui/dialog/calligraphic-profile-rename.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+std::vector<Glib::ustring> get_presets_list() {
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ std::vector<Glib::ustring> presets = prefs->getAllDirs("/tools/calligraphic/preset");
+
+ return presets;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+CalligraphyToolbar::CalligraphyToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _presets_blocked(false)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /*calligraphic profile */
+ {
+ _profile_selector_combo = Gtk::manage(new Gtk::ComboBoxText());
+ _profile_selector_combo->set_tooltip_text(_("Choose a preset"));
+
+ build_presets_list();
+
+ auto profile_selector_ti = Gtk::manage(new Gtk::ToolItem());
+ profile_selector_ti->add(*_profile_selector_combo);
+ add(*profile_selector_ti);
+
+ _profile_selector_combo->signal_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::change_profile));
+ }
+
+ /*calligraphic profile editor */
+ {
+ auto profile_edit_item = Gtk::manage(new Gtk::ToolButton(_("Add/Edit Profile")));
+ profile_edit_item->set_tooltip_text(_("Add or edit calligraphic profile"));
+ profile_edit_item->set_icon_name(INKSCAPE_ICON("document-properties"));
+ profile_edit_item->signal_clicked().connect(sigc::mem_fun(*this, &CalligraphyToolbar::edit_profile));
+ add(*profile_edit_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Width */
+ std::vector<Glib::ustring> labels = {_("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")};
+ std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100};
+ auto width_val = prefs->getDouble("/tools/calligraphic/width", 15);
+ _width_adj = Gtk::Adjustment::create(width_val, 1, 100, 1.0, 10.0);
+ auto width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-width", _("Width:"), _width_adj, 1, 0));
+ width_item->set_tooltip_text(_("The width of the calligraphic pen (relative to the visible canvas area)"));
+ width_item->set_custom_numeric_menu_data(values, labels);
+ width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::width_value_changed));
+ _widget_map["width"] = G_OBJECT(_width_adj->gobj());
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*width_item);
+ width_item->set_sensitive(true);
+ }
+
+ /* Use Pressure button */
+ {
+ _usepressure = add_toggle_button(_("Pressure"),
+ _("Use the pressure of the input device to alter the width of the pen"));
+ _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _widget_map["usepressure"] = G_OBJECT(_usepressure->gobj());
+ _usepressure_pusher.reset(new SimplePrefPusher(_usepressure, "/tools/calligraphic/usepressure"));
+ _usepressure->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled),
+ _usepressure,
+ "/tools/calligraphic/usepressure"));
+ }
+
+ /* Trace Background button */
+ {
+ _tracebackground = add_toggle_button(_("Trace Background"),
+ _("Trace the lightness of the background by the width of the pen (white - minimum width, black - maximum width)"));
+ _tracebackground->set_icon_name(INKSCAPE_ICON("draw-trace-background"));
+ _widget_map["tracebackground"] = G_OBJECT(_tracebackground->gobj());
+ _tracebackground_pusher.reset(new SimplePrefPusher(_tracebackground, "/tools/calligraphic/tracebackground"));
+ _tracebackground->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled),
+ _tracebackground,
+ "/tools/calligraphic/tracebackground"));
+ }
+
+ {
+ /* Thinning */
+ std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")};
+ std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100};
+ auto thinning_val = prefs->getDouble("/tools/calligraphic/thinning", 10);
+ _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0);
+ auto thinning_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-thinning", _("Thinning:"), _thinning_adj, 1, 0));
+ thinning_item->set_tooltip_text(("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)"));
+ thinning_item->set_custom_numeric_menu_data(values, labels);
+ thinning_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::velthin_value_changed));
+ _widget_map["thinning"] = G_OBJECT(_thinning_adj->gobj());
+ add(*thinning_item);
+ thinning_item->set_sensitive(true);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Angle */
+ std::vector<Glib::ustring> labels = {_("(left edge up)"), "", "", _("(horizontal)"), _("(default)"), "", _("(right edge up)")};
+ std::vector<double> values = { -90, -60, -30, 0, 30, 60, 90};
+ auto angle_val = prefs->getDouble("/tools/calligraphic/angle", 30);
+ _angle_adj = Gtk::Adjustment::create(angle_val, -90.0, 90.0, 1.0, 10.0);
+ _angle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-angle", _("Angle:"), _angle_adj, 1, 0));
+ _angle_item->set_tooltip_text(_("The angle of the pen's nib (in degrees; 0 = horizontal; has no effect if fixation = 0)"));
+ _angle_item->set_custom_numeric_menu_data(values, labels);
+ _angle_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _angle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::angle_value_changed));
+ _widget_map["angle"] = G_OBJECT(_angle_adj->gobj());
+ add(*_angle_item);
+ _angle_item->set_sensitive(true);
+ }
+
+ /* Use Tilt button */
+ {
+ _usetilt = add_toggle_button(_("Tilt"),
+ _("Use the tilt of the input device to alter the angle of the pen's nib"));
+ _usetilt->set_icon_name(INKSCAPE_ICON("draw-use-tilt"));
+ _widget_map["usetilt"] = G_OBJECT(_usetilt->gobj());
+ _usetilt_pusher.reset(new SimplePrefPusher(_usetilt, "/tools/calligraphic/usetilt"));
+ _usetilt->signal_toggled().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tilt_state_changed));
+ _angle_item->set_sensitive(!prefs->getBool("/tools/calligraphic/usetilt", true));
+ _usetilt->set_active(prefs->getBool("/tools/calligraphic/usetilt", true));
+ }
+
+ {
+ /* Fixation */
+ std::vector<Glib::ustring> labels = {_("(perpendicular to stroke, \"brush\")"), "", "", "", _("(almost fixed, default)"), _("(fixed by Angle, \"pen\")")};
+ std::vector<double> values = { 0, 20, 40, 60, 90, 100};
+ auto flatness_val = prefs->getDouble("/tools/calligraphic/flatness", 90);
+ _fixation_adj = Gtk::Adjustment::create(flatness_val, 0.0, 100, 1.0, 10.0);
+ auto flatness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-fixation", _("Fixation:"), _fixation_adj, 1, 0));
+ flatness_item->set_tooltip_text(_("Angle behavior (0 = nib always perpendicular to stroke direction, 100 = fixed angle)"));
+ flatness_item->set_custom_numeric_menu_data(values, labels);
+ flatness_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _fixation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::flatness_value_changed));
+ _widget_map["flatness"] = G_OBJECT(_fixation_adj->gobj());
+ add(*flatness_item);
+ flatness_item->set_sensitive(true);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Cap Rounding */
+ std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")};
+ std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0};
+ auto cap_rounding_val = prefs->getDouble("/tools/calligraphic/cap_rounding", 0.0);
+ _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1);
+ auto cap_rounding_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2));
+
+ // TRANSLATORS: "cap" means "end" (both start and finish) here
+ cap_rounding_item->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)"));
+ cap_rounding_item->set_custom_numeric_menu_data(values, labels);
+ cap_rounding_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::cap_rounding_value_changed));
+ _widget_map["cap_rounding"] = G_OBJECT(_cap_rounding_adj->gobj());
+ add(*cap_rounding_item);
+ cap_rounding_item->set_sensitive(true);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Tremor */
+ std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")};
+ std::vector<double> values = { 0, 10, 20, 40, 60, 100};
+ auto tremor_val = prefs->getDouble("/tools/calligraphic/tremor", 0.0);
+ _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0);
+ auto tremor_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-tremor", _("Tremor:"), _tremor_adj, 1, 0));
+ tremor_item->set_tooltip_text(_("Increase to make strokes rugged and trembling"));
+ tremor_item->set_custom_numeric_menu_data(values, labels);
+ tremor_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tremor_value_changed));
+ _widget_map["tremor"] = G_OBJECT(_tremor_adj->gobj());
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*tremor_item);
+ tremor_item->set_sensitive(true);
+ }
+
+ {
+ /* Wiggle */
+ std::vector<Glib::ustring> labels = {_("(no wiggle)"), _("(slight deviation)"), "", "", _("(wild waves and curls)")};
+ std::vector<double> values = { 0, 20, 40, 60, 100};
+ auto wiggle_val = prefs->getDouble("/tools/calligraphic/wiggle", 0.0);
+ _wiggle_adj = Gtk::Adjustment::create(wiggle_val, 0.0, 100, 1, 10.0);
+ auto wiggle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-wiggle", _("Wiggle:"), _wiggle_adj, 1, 0));
+ wiggle_item->set_tooltip_text(_("Increase to make the pen waver and wiggle"));
+ wiggle_item->set_custom_numeric_menu_data(values, labels);
+ wiggle_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _wiggle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::wiggle_value_changed));
+ _widget_map["wiggle"] = G_OBJECT(_wiggle_adj->gobj());
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*wiggle_item);
+ wiggle_item->set_sensitive(true);
+ }
+
+ {
+ /* Mass */
+ std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")};
+ std::vector<double> values = { 0.0, 2, 10, 20, 50, 100};
+ auto mass_val = prefs->getDouble("/tools/calligraphic/mass", 2.0);
+ _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0);
+ auto mass_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-mass", _("Mass:"), _mass_adj, 1, 0));
+ mass_item->set_tooltip_text(_("Increase to make the pen drag behind, as if slowed by inertia"));
+ mass_item->set_custom_numeric_menu_data(values, labels);
+ mass_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::mass_value_changed));
+ _widget_map["mass"] = G_OBJECT(_mass_adj->gobj());
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*mass_item);
+ mass_item->set_sensitive(true);
+ }
+
+ show_all();
+}
+
+GtkWidget *
+CalligraphyToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new CalligraphyToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+CalligraphyToolbar::width_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/width", _width_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::velthin_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/calligraphic/thinning", _thinning_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::angle_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/angle", _angle_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::flatness_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/flatness", _fixation_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::cap_rounding_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/cap_rounding", _cap_rounding_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::tremor_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/tremor", _tremor_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::wiggle_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/wiggle", _wiggle_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::mass_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/calligraphic/mass", _mass_adj->get_value() );
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::on_pref_toggled(Gtk::ToggleToolButton *item,
+ const Glib::ustring& path)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(path, item->get_active());
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::update_presets_list()
+{
+ if (_presets_blocked) {
+ return;
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ auto presets = get_presets_list();
+
+ int index = 1; // 0 is for no preset.
+ for (auto i = presets.begin(); i != presets.end(); ++i, ++index) {
+ bool match = true;
+
+ auto preset = prefs->getAllEntries(*i);
+ for (auto & j : preset) {
+ Glib::ustring entry_name = j.getEntryName();
+ if (entry_name == "id" || entry_name == "name") {
+ continue;
+ }
+
+ void *widget = _widget_map[entry_name.data()];
+ if (widget) {
+ if (GTK_IS_ADJUSTMENT(widget)) {
+ double v = j.getDouble();
+ GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget);
+ //std::cout << "compared adj " << attr_name << gtk_adjustment_get_value(adj) << " to " << v << "\n";
+ if (fabs(gtk_adjustment_get_value(adj) - v) > 1e-6) {
+ match = false;
+ break;
+ }
+ } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) {
+ bool v = j.getBool();
+ auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget);
+ //std::cout << "compared toggle " << attr_name << gtk_toggle_action_get_active(toggle) << " to " << v << "\n";
+ if ( static_cast<bool>(gtk_toggle_tool_button_get_active(toggle)) != v ) {
+ match = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (match) {
+ // newly added item is at the same index as the
+ // save command, so we need to change twice for it to take effect
+ _profile_selector_combo->set_active(0);
+ _profile_selector_combo->set_active(index);
+ return;
+ }
+ }
+
+ // no match found
+ _profile_selector_combo->set_active(0);
+}
+
+void
+CalligraphyToolbar::tilt_state_changed()
+{
+ _angle_item->set_sensitive(!_usetilt->get_active());
+ on_pref_toggled(_usetilt, "/tools/calligraphic/usetilt");
+}
+
+void
+CalligraphyToolbar::build_presets_list()
+{
+ _presets_blocked = true;
+
+ _profile_selector_combo->remove_all();
+ _profile_selector_combo->append(_("No preset"));
+
+ // iterate over all presets to populate the list
+ auto prefs = Inkscape::Preferences::get();
+ auto presets = get_presets_list();
+
+ for (auto & preset : presets) {
+ GtkTreeIter iter;
+ Glib::ustring preset_name = prefs->getString(preset + "/name");
+
+ if (!preset_name.empty()) {
+ _profile_selector_combo->append(_(preset_name.data()));
+ }
+ }
+
+ _presets_blocked = false;
+
+ update_presets_list();
+}
+
+void
+CalligraphyToolbar::change_profile()
+{
+ auto mode = _profile_selector_combo->get_active_row_number();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (_presets_blocked) {
+ return;
+ }
+
+ // mode is one-based so we subtract 1
+ std::vector<Glib::ustring> presets = get_presets_list();
+
+ Glib::ustring preset_path = "";
+ if (mode - 1 < presets.size()) {
+ preset_path = presets.at(mode - 1);
+ }
+
+ if (!preset_path.empty()) {
+ _presets_blocked = true; //temporarily block the selector so no one will updadte it while we're reading it
+
+ std::vector<Inkscape::Preferences::Entry> preset = prefs->getAllEntries(preset_path);
+
+ // Shouldn't this be std::map?
+ for (auto & i : preset) {
+ Glib::ustring entry_name = i.getEntryName();
+ if (entry_name == "id" || entry_name == "name") {
+ continue;
+ }
+ void *widget = _widget_map[entry_name.data()];
+ if (widget) {
+ if (GTK_IS_ADJUSTMENT(widget)) {
+ GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget);
+ gtk_adjustment_set_value(adj, i.getDouble());
+ //std::cout << "set adj " << attr_name << " to " << v << "\n";
+ } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) {
+ auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget);
+ gtk_toggle_tool_button_set_active(toggle, i.getBool());
+ //std::cout << "set toggle " << attr_name << " to " << v << "\n";
+ } else {
+ g_warning("Unknown widget type for preset: %s\n", entry_name.data());
+ }
+ } else {
+ g_warning("Bad key found in a preset record: %s\n", entry_name.data());
+ }
+ }
+ _presets_blocked = false;
+ }
+}
+
+void
+CalligraphyToolbar::edit_profile()
+{
+ save_profile(nullptr);
+}
+
+void
+CalligraphyToolbar::save_profile(GtkWidget * /*widget*/)
+{
+ using Inkscape::UI::Dialog::CalligraphicProfileRename;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (! _desktop) {
+ return;
+ }
+
+ if (_presets_blocked) {
+ return;
+ }
+
+ Glib::ustring current_profile_name = _profile_selector_combo->get_active_text();
+
+ if (current_profile_name == _("No preset")) {
+ current_profile_name = "";
+ }
+
+ CalligraphicProfileRename::show(_desktop, current_profile_name);
+ if ( !CalligraphicProfileRename::applied()) {
+ // dialog cancelled
+ update_presets_list();
+ return;
+ }
+ Glib::ustring new_profile_name = CalligraphicProfileRename::getProfileName();
+
+ if (new_profile_name.empty()) {
+ // empty name entered
+ update_presets_list ();
+ return;
+ }
+
+ _presets_blocked = true;
+
+ // If there's a preset with the given name, find it and set save_path appropriately
+ auto presets = get_presets_list();
+ int total_presets = presets.size();
+ int new_index = -1;
+ Glib::ustring save_path; // profile pref path without a trailing slash
+
+ int temp_index = 0;
+ for (std::vector<Glib::ustring>::iterator i = presets.begin(); i != presets.end(); ++i, ++temp_index) {
+ Glib::ustring name = prefs->getString(*i + "/name");
+ if (!name.empty() && (new_profile_name == name || current_profile_name == name)) {
+ new_index = temp_index;
+ save_path = *i;
+ break;
+ }
+ }
+
+ if ( CalligraphicProfileRename::deleted() && new_index != -1) {
+ prefs->remove(save_path);
+ _presets_blocked = false;
+ build_presets_list();
+ return;
+ }
+
+ if (new_index == -1) {
+ // no preset with this name, create
+ new_index = total_presets + 1;
+ gchar *profile_id = g_strdup_printf("/dcc%d", new_index);
+ save_path = Glib::ustring("/tools/calligraphic/preset") + profile_id;
+ g_free(profile_id);
+ }
+
+ for (auto map_item : _widget_map) {
+ auto widget_name = map_item.first;
+ auto widget = map_item.second;
+
+ if (widget) {
+ if (GTK_IS_ADJUSTMENT(widget)) {
+ GtkAdjustment* adj = GTK_ADJUSTMENT(widget);
+ prefs->setDouble(save_path + "/" + widget_name, gtk_adjustment_get_value(adj));
+ //std::cout << "wrote adj " << widget_name << ": " << v << "\n";
+ } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) {
+ auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget);
+ prefs->setBool(save_path + "/" + widget_name, gtk_toggle_tool_button_get_active(toggle));
+ //std::cout << "wrote tog " << widget_name << ": " << v << "\n";
+ } else {
+ g_warning("Unknown widget type for preset: %s\n", widget_name.c_str());
+ }
+ } else {
+ g_warning("Bad key when writing preset: %s\n", widget_name.c_str());
+ }
+ }
+ prefs->setString(save_path + "/name", new_profile_name);
+
+ _presets_blocked = true;
+ build_presets_list();
+}
+
+}
+}
+}
+
+
+/*
+ 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/toolbar/calligraphy-toolbar.h b/src/ui/toolbar/calligraphy-toolbar.h
new file mode 100644
index 0000000..d216888
--- /dev/null
+++ b/src/ui/toolbar/calligraphy-toolbar.h
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CALLIGRAPHY_TOOLBAR_H
+#define SEEN_CALLIGRAPHY_TOOLBAR_H
+
+/**
+ * @file
+ * Calligraphy aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ComboBoxText;
+}
+
+namespace Inkscape {
+namespace UI {
+class SimplePrefPusher;
+
+namespace Widget {
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+
+class CalligraphyToolbar : public Toolbar {
+private:
+ bool _presets_blocked;
+
+ UI::Widget::SpinButtonToolItem *_angle_item;
+ Gtk::ComboBoxText *_profile_selector_combo;
+
+ std::map<Glib::ustring, GObject *> _widget_map;
+
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _mass_adj;
+ Glib::RefPtr<Gtk::Adjustment> _wiggle_adj;
+ Glib::RefPtr<Gtk::Adjustment> _angle_adj;
+ Glib::RefPtr<Gtk::Adjustment> _thinning_adj;
+ Glib::RefPtr<Gtk::Adjustment> _tremor_adj;
+ Glib::RefPtr<Gtk::Adjustment> _fixation_adj;
+ Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj;
+ Gtk::ToggleToolButton *_usepressure;
+ Gtk::ToggleToolButton *_tracebackground;
+ Gtk::ToggleToolButton *_usetilt;
+
+ std::unique_ptr<SimplePrefPusher> _tracebackground_pusher;
+ std::unique_ptr<SimplePrefPusher> _usepressure_pusher;
+ std::unique_ptr<SimplePrefPusher> _usetilt_pusher;
+
+ void width_value_changed();
+ void velthin_value_changed();
+ void angle_value_changed();
+ void flatness_value_changed();
+ void cap_rounding_value_changed();
+ void tremor_value_changed();
+ void wiggle_value_changed();
+ void mass_value_changed();
+ void build_presets_list();
+ void change_profile();
+ void save_profile(GtkWidget *widget);
+ void edit_profile();
+ void update_presets_list();
+ void tilt_state_changed();
+ void on_pref_toggled(Gtk::ToggleToolButton *item,
+ const Glib::ustring& path);
+
+protected:
+ CalligraphyToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_CALLIGRAPHY_TOOLBAR_H */
diff --git a/src/ui/toolbar/connector-toolbar.cpp b/src/ui/toolbar/connector-toolbar.cpp
new file mode 100644
index 0000000..0220a0a
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.cpp
@@ -0,0 +1,437 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Connector aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "connector-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/separatortoolitem.h>
+
+#include "conn-avoid-ref.h"
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "enums.h"
+#include "graphlayout.h"
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/connector-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "widgets/spinbutton-events.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector connector_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::ConnectorToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ConnectorToolbar::ConnectorToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _freeze(false),
+ _repr(nullptr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ {
+ auto avoid_item = Gtk::manage(new Gtk::ToolButton(_("Avoid")));
+ avoid_item->set_tooltip_text(_("Make connectors avoid selected objects"));
+ avoid_item->set_icon_name(INKSCAPE_ICON("connector-avoid"));
+ avoid_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_avoid));
+ add(*avoid_item);
+ }
+
+ {
+ auto ignore_item = Gtk::manage(new Gtk::ToolButton(_("Ignore")));
+ ignore_item->set_tooltip_text(_("Make connectors ignore selected objects"));
+ ignore_item->set_icon_name(INKSCAPE_ICON("connector-ignore"));
+ ignore_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_ignore));
+ add(*ignore_item);
+ }
+
+ // Orthogonal connectors toggle button
+ {
+ _orthogonal = add_toggle_button(_("Orthogonal"),
+ _("Make connector orthogonal or polyline"));
+ _orthogonal->set_icon_name(INKSCAPE_ICON("connector-orthogonal"));
+
+ bool tbuttonstate = prefs->getBool("/tools/connector/orthogonal");
+ _orthogonal->set_active(( tbuttonstate ? TRUE : FALSE ));
+ _orthogonal->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::orthogonal_toggled));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ // Curvature spinbox
+ auto curvature_val = prefs->getDouble("/tools/connector/curvature", defaultConnCurvature);
+ _curvature_adj = Gtk::Adjustment::create(curvature_val, 0, 100, 1.0, 10.0);
+ auto curvature_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-curvature", _("Curvature:"), _curvature_adj, 1, 0));
+ curvature_item->set_tooltip_text(_("The amount of connectors curvature"));
+ curvature_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _curvature_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::curvature_changed));
+ add(*curvature_item);
+
+ // Spacing spinbox
+ auto spacing_val = prefs->getDouble("/tools/connector/spacing", defaultConnSpacing);
+ _spacing_adj = Gtk::Adjustment::create(spacing_val, 0, 100, 1.0, 10.0);
+ auto spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-spacing", _("Spacing:"), _spacing_adj, 1, 0));
+ spacing_item->set_tooltip_text(_("The amount of space left around objects by auto-routing connectors"));
+ spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::spacing_changed));
+ add(*spacing_item);
+
+ // Graph (connector network) layout
+ {
+ auto graph_item = Gtk::manage(new Gtk::ToolButton(_("Graph")));
+ graph_item->set_tooltip_text(_("Nicely arrange selected connector network"));
+ graph_item->set_icon_name(INKSCAPE_ICON("distribute-graph"));
+ graph_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::graph_layout));
+ add(*graph_item);
+ }
+
+ // Default connector length spinbox
+ auto length_val = prefs->getDouble("/tools/connector/length", 100);
+ _length_adj = Gtk::Adjustment::create(length_val, 10, 1000, 10.0, 100.0);
+ auto length_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-length", _("Length:"), _length_adj, 1, 0));
+ length_item->set_tooltip_text(_("Ideal length for connectors when layout is applied"));
+ length_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _length_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::length_changed));
+ add(*length_item);
+
+ // Directed edges toggle button
+ {
+ _directed_item = add_toggle_button(_("Downwards"),
+ _("Make connectors with end-markers (arrows) point downwards"));
+ _directed_item->set_icon_name(INKSCAPE_ICON("distribute-graph-directed"));
+
+ bool tbuttonstate = prefs->getBool("/tools/connector/directedlayout");
+ _directed_item->set_active(tbuttonstate ? TRUE : FALSE);
+
+ _directed_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::directed_graph_layout_toggled));
+ desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ConnectorToolbar::selection_changed));
+ }
+
+ // Avoid overlaps toggle button
+ {
+ _overlap_item = add_toggle_button(_("Remove overlaps"),
+ _("Do not allow overlapping shapes"));
+ _overlap_item->set_icon_name(INKSCAPE_ICON("distribute-remove-overlaps"));
+
+ bool tbuttonstate = prefs->getBool("/tools/connector/avoidoverlaplayout");
+ _overlap_item->set_active(tbuttonstate ? TRUE : FALSE);
+
+ _overlap_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::nooverlaps_graph_layout_toggled));
+ }
+
+ // Code to watch for changes to the connector-spacing attribute in
+ // the XML.
+ Inkscape::XML::Node *repr = desktop->namedview->getRepr();
+ g_assert(repr != nullptr);
+
+ if(_repr) {
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&connector_tb_repr_events, this);
+ _repr->synthesizeEvents(&connector_tb_repr_events, this);
+ }
+
+ show_all();
+}
+
+GtkWidget *
+ConnectorToolbar::create( SPDesktop *desktop)
+{
+ auto toolbar = new ConnectorToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+} // end of ConnectorToolbar::prep()
+
+void
+ConnectorToolbar::path_set_avoid()
+{
+ Inkscape::UI::Tools::cc_selection_set_avoid(true);
+}
+
+void
+ConnectorToolbar::path_set_ignore()
+{
+ Inkscape::UI::Tools::cc_selection_set_avoid(false);
+}
+
+void
+ConnectorToolbar::orthogonal_toggled()
+{
+ auto doc = _desktop->getDocument();
+
+ if (!DocumentUndo::getUndoSensitive(doc)) {
+ return;
+ }
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ bool is_orthog = _orthogonal->get_active();
+ gchar orthog_str[] = "orthogonal";
+ gchar polyline_str[] = "polyline";
+ gchar *value = is_orthog ? orthog_str : polyline_str ;
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+
+ if (Inkscape::UI::Tools::cc_item_is_connector(item)) {
+ item->setAttribute( "inkscape:connector-type",
+ value, nullptr);
+ item->getAvoidRef().handleSettingChange();
+ modmade = true;
+ }
+ }
+
+ if (!modmade) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/connector/orthogonal", is_orthog);
+ } else {
+
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR,
+ is_orthog ? _("Set connector type: orthogonal"): _("Set connector type: polyline"));
+ }
+
+ _freeze = false;
+}
+
+void
+ConnectorToolbar::curvature_changed()
+{
+ SPDocument *doc = _desktop->getDocument();
+
+ if (!DocumentUndo::getUndoSensitive(doc)) {
+ return;
+ }
+
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ auto newValue = _curvature_adj->get_value();
+ gchar value[G_ASCII_DTOSTR_BUF_SIZE];
+ g_ascii_dtostr(value, G_ASCII_DTOSTR_BUF_SIZE, newValue);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+
+ if (Inkscape::UI::Tools::cc_item_is_connector(item)) {
+ item->setAttribute( "inkscape:connector-curvature",
+ value, nullptr);
+ item->getAvoidRef().handleSettingChange();
+ modmade = true;
+ }
+ }
+
+ if (!modmade) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/connector/curvature"), newValue);
+ }
+ else {
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR,
+ _("Change connector curvature"));
+ }
+
+ _freeze = false;
+}
+
+void
+ConnectorToolbar::spacing_changed()
+{
+ SPDocument *doc = _desktop->getDocument();
+
+ if (!DocumentUndo::getUndoSensitive(doc)) {
+ return;
+ }
+
+ Inkscape::XML::Node *repr = _desktop->namedview->getRepr();
+
+ if ( !repr->attribute("inkscape:connector-spacing") &&
+ ( _spacing_adj->get_value() == defaultConnSpacing )) {
+ // Don't need to update the repr if the attribute doesn't
+ // exist and it is being set to the default value -- as will
+ // happen at startup.
+ return;
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ sp_repr_set_css_double(repr, "inkscape:connector-spacing", _spacing_adj->get_value());
+ _desktop->namedview->updateRepr();
+ bool modmade = false;
+
+ std::vector<SPItem *> items;
+ items = get_avoided_items(items, _desktop->currentRoot(), _desktop);
+ for (auto item : items) {
+ Geom::Affine m = Geom::identity();
+ avoid_item_move(&m, item);
+ modmade = true;
+ }
+
+ if(modmade) {
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR,
+ _("Change connector spacing"));
+ }
+ _freeze = false;
+}
+
+void
+ConnectorToolbar::graph_layout()
+{
+ if (!SP_ACTIVE_DESKTOP) {
+ return;
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // hack for clones, see comment in align-and-distribute.cpp
+ int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
+ prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
+
+ auto tmp = SP_ACTIVE_DESKTOP->getSelection()->items();
+ std::vector<SPItem *> vec(tmp.begin(), tmp.end());
+ graphlayout(vec);
+
+ prefs->setInt("/options/clonecompensation/value", saved_compensation);
+
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Arrange connector network"));
+}
+
+void
+ConnectorToolbar::length_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/connector/length", _length_adj->get_value());
+}
+
+void
+ConnectorToolbar::directed_graph_layout_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/connector/directedlayout", _directed_item->get_active());
+}
+
+void
+ConnectorToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ SPItem *item = selection->singleItem();
+ if (SP_IS_PATH(item))
+ {
+ gdouble curvature = SP_PATH(item)->connEndPair.getCurvature();
+ bool is_orthog = SP_PATH(item)->connEndPair.isOrthogonal();
+ _orthogonal->set_active(is_orthog);
+ _curvature_adj->set_value(curvature);
+ }
+
+}
+
+void
+ConnectorToolbar::nooverlaps_graph_layout_toggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/connector/avoidoverlaplayout",
+ _overlap_item->get_active());
+}
+
+void
+ConnectorToolbar::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<ConnectorToolbar *>(data);
+
+ if ( !toolbar->_freeze
+ && (strcmp(name, "inkscape:connector-spacing") == 0) ) {
+ gdouble spacing = defaultConnSpacing;
+ sp_repr_get_double(repr, "inkscape:connector-spacing", &spacing);
+
+ toolbar->_spacing_adj->set_value(spacing);
+
+ if(toolbar->_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(toolbar->_desktop->canvas));
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/connector-toolbar.h b/src/ui/toolbar/connector-toolbar.h
new file mode 100644
index 0000000..66df79e
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.h
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CONNECTOR_TOOLBAR_H
+#define SEEN_CONNECTOR_TOOLBAR_H
+
+/**
+ * @file
+ * Connector aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Toolbar {
+class ConnectorToolbar : public Toolbar {
+private:
+ Gtk::ToggleToolButton *_orthogonal;
+ Gtk::ToggleToolButton *_directed_item;
+ Gtk::ToggleToolButton *_overlap_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _curvature_adj;
+ Glib::RefPtr<Gtk::Adjustment> _spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _length_adj;
+
+ bool _freeze;
+
+ Inkscape::XML::Node *_repr;
+
+ void path_set_avoid();
+ void path_set_ignore();
+ void orthogonal_toggled();
+ void graph_layout();
+ void directed_graph_layout_toggled();
+ void nooverlaps_graph_layout_toggled();
+ void curvature_changed();
+ void spacing_changed();
+ void length_changed();
+ void selection_changed(Inkscape::Selection *selection);
+
+protected:
+ ConnectorToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_CONNECTOR_TOOLBAR_H */
diff --git a/src/ui/toolbar/dropper-toolbar.cpp b/src/ui/toolbar/dropper-toolbar.cpp
new file mode 100644
index 0000000..3d6c4f9
--- /dev/null
+++ b/src/ui/toolbar/dropper-toolbar.cpp
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dropper aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include "dropper-toolbar.h"
+#include "document-undo.h"
+#include "preferences.h"
+#include "widgets/spinbutton-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+void DropperToolbar::on_pick_alpha_button_toggled()
+{
+ auto active = _pick_alpha_button->get_active();
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt( "/tools/dropper/pick", active );
+
+ _set_alpha_button->set_sensitive(active);
+
+ spinbutton_defocus(GTK_WIDGET(gobj()));
+}
+
+void DropperToolbar::on_set_alpha_button_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool( "/tools/dropper/setalpha", _set_alpha_button->get_active( ) );
+ spinbutton_defocus(GTK_WIDGET(gobj()));
+}
+
+/*
+ * TODO: Would like to add swatch of current color.
+ * TODO: Add queue of last 5 or so colors selected with new swatches so that
+ * can drag and drop places. Will provide a nice mixing palette.
+ */
+DropperToolbar::DropperToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ // Add widgets to toolbar
+ add_label(_("Opacity:"));
+ _pick_alpha_button = add_toggle_button(_("Pick"),
+ _("Pick both the color and the alpha (transparency) under cursor; "
+ "otherwise, pick only the visible color premultiplied by alpha"));
+ _set_alpha_button = add_toggle_button(_("Assign"),
+ _("If alpha was picked, assign it to selection "
+ "as fill or stroke transparency"));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Set initial state of widgets
+ auto pickAlpha = prefs->getInt( "/tools/dropper/pick", 1 );
+ auto setAlpha = prefs->getBool( "/tools/dropper/setalpha", true);
+
+ _pick_alpha_button->set_active(pickAlpha);
+ _set_alpha_button->set_active(setAlpha);
+
+ // Make sure the set-alpha button is disabled if we're not picking alpha
+ _set_alpha_button->set_sensitive(pickAlpha);
+
+ // Connect signal handlers
+ auto pick_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_pick_alpha_button_toggled);
+ auto set_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_set_alpha_button_toggled);
+
+ _pick_alpha_button->signal_toggled().connect(pick_alpha_button_toggled_cb);
+ _set_alpha_button->signal_toggled().connect(set_alpha_button_toggled_cb);
+
+ show_all();
+}
+
+GtkWidget *
+DropperToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = Gtk::manage(new DropperToolbar(desktop));
+ return GTK_WIDGET(toolbar->gobj());
+}
+}
+}
+}
+
+/*
+ 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/toolbar/dropper-toolbar.h b/src/ui/toolbar/dropper-toolbar.h
new file mode 100644
index 0000000..c8aa42f
--- /dev/null
+++ b/src/ui/toolbar/dropper-toolbar.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_DROPPER_TOOLBAR_H
+#define SEEN_DROPPER_TOOLBAR_H
+
+/**
+ * @file
+ * Dropper aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+/**
+ * \brief A toolbar for controlling the dropper tool
+ */
+class DropperToolbar : public Toolbar {
+private:
+ // Tool widgets
+ Gtk::ToggleToolButton *_pick_alpha_button; ///< Control whether to pick opacity
+ Gtk::ToggleToolButton *_set_alpha_button; ///< Control whether to set opacity
+
+ // Event handlers
+ void on_pick_alpha_button_toggled();
+ void on_set_alpha_button_toggled();
+
+protected:
+ DropperToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+}
+}
+}
+#endif /* !SEEN_DROPPER_TOOLBAR_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/toolbar/eraser-toolbar.cpp b/src/ui/toolbar/eraser-toolbar.cpp
new file mode 100644
index 0000000..279b82d
--- /dev/null
+++ b/src/ui/toolbar/eraser-toolbar.cpp
@@ -0,0 +1,335 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Erasor aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "eraser-toolbar.h"
+
+#include <array>
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+#include "ui/tools/eraser-tool.h"
+
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+EraserToolbar::EraserToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _freeze(false)
+{
+ gint eraser_mode = ERASER_MODE_DELETE;
+ auto prefs = Inkscape::Preferences::get();
+
+ // Mode
+ {
+ add_label(_("Mode:"));
+
+ Gtk::RadioToolButton::Group mode_group;
+
+ std::vector<Gtk::RadioToolButton *> mode_buttons;
+
+ auto delete_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete")));
+ delete_btn->set_tooltip_text(_("Delete objects touched by eraser"));
+ delete_btn->set_icon_name(INKSCAPE_ICON("draw-eraser-delete-objects"));
+ mode_buttons.push_back(delete_btn);
+
+ auto cut_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Cut")));
+ cut_btn->set_tooltip_text(_("Cut out from paths and shapes"));
+ cut_btn->set_icon_name(INKSCAPE_ICON("path-difference"));
+ mode_buttons.push_back(cut_btn);
+
+ auto clip_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Clip")));
+ clip_btn->set_tooltip_text(_("Clip from objects"));
+ clip_btn->set_icon_name(INKSCAPE_ICON("path-intersection"));
+ mode_buttons.push_back(clip_btn);
+
+ eraser_mode = prefs->getInt("/tools/eraser/mode", ERASER_MODE_CLIP); // Used at end
+
+ mode_buttons[eraser_mode]->set_active();
+
+ int btn_index = 0;
+
+ for (auto btn : mode_buttons)
+ {
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &EraserToolbar::mode_changed), btn_index++));
+ }
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Width */
+ {
+ std::vector<Glib::ustring> labels = {_("(no width)"), _("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")};
+ std::vector<double> values = { 0, 1, 3, 5, 10, 15, 20, 30, 50, 75, 100};
+ auto width_val = prefs->getDouble("/tools/eraser/width", 15);
+ _width_adj = Gtk::Adjustment::create(width_val, 0, 100, 1.0, 10.0);
+ _width = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-width", _("Width:"), _width_adj, 1, 0));
+ _width->set_tooltip_text(_("The width of the eraser pen (relative to the visible canvas area)"));
+ _width->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _width->set_custom_numeric_menu_data(values, labels);
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::width_value_changed));
+ // TODO: Allow SpinButtonToolItem to display as a slider
+ // ege_adjustment_action_set_appearance( toolbar->_width, TOOLBAR_SLIDER_HINT );
+ add(*_width);
+ _width->set_sensitive(true);
+ }
+
+ /* Use Pressure button */
+ {
+ _usepressure = add_toggle_button(_("Eraser Pressure"),
+ _("Use the pressure of the input device to alter the width of the pen"));
+ _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _pressure_pusher.reset(new UI::SimplePrefPusher(_usepressure, "/tools/eraser/usepressure"));
+ _usepressure->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::usepressure_toggled));
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Thinning */
+ {
+ std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")};
+ std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100};
+ auto thinning_val = prefs->getDouble("/tools/eraser/thinning", 10);
+ _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0);
+ _thinning = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-thinning", _("Thinning:"), _thinning_adj, 1, 0));
+ _thinning->set_tooltip_text(_("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)"));
+ _thinning->set_custom_numeric_menu_data(values, labels);
+ _thinning->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::velthin_value_changed));
+ add(*_thinning);
+ _thinning->set_sensitive(true);
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Cap Rounding */
+ {
+ std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")};
+ std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0};
+ auto cap_rounding_val = prefs->getDouble("/tools/eraser/cap_rounding", 0.0);
+ _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1);
+ // TRANSLATORS: "cap" means "end" (both start and finish) here
+ _cap_rounding = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2));
+ _cap_rounding->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)"));
+ _cap_rounding->set_custom_numeric_menu_data(values, labels);
+ _cap_rounding->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::cap_rounding_value_changed));
+ add(*_cap_rounding);
+ _cap_rounding->set_sensitive(true);
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Tremor */
+ {
+ std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")};
+ std::vector<double> values = { 0, 10, 20, 40, 60, 100};
+ auto tremor_val = prefs->getDouble("/tools/eraser/tremor", 0.0);
+ _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0);
+ _tremor = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-tremor", _("Tremor:"), _tremor_adj, 1, 0));
+ _tremor->set_tooltip_text(_("Increase to make strokes rugged and trembling"));
+ _tremor->set_custom_numeric_menu_data(values, labels);
+ _tremor->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::tremor_value_changed));
+
+ // TODO: Allow slider appearance
+ //ege_adjustment_action_set_appearance( toolbar->_tremor, TOOLBAR_SLIDER_HINT );
+ add(*_tremor);
+ _tremor->set_sensitive(true);
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Mass */
+ {
+ std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")};
+ std::vector<double> values = { 0.0, 2, 10, 20, 50, 100};
+ auto mass_val = prefs->getDouble("/tools/eraser/mass", 10.0);
+ _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0);
+ _mass = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-mass", _("Mass:"), _mass_adj, 1, 0));
+ _mass->set_tooltip_text(_("Increase to make the eraser drag behind, as if slowed by inertia"));
+ _mass->set_custom_numeric_menu_data(values, labels);
+ _mass->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::mass_value_changed));
+ // TODO: Allow slider appearance
+ //ege_adjustment_action_set_appearance( toolbar->_mass, TOOLBAR_SLIDER_HINT );
+ add(*_mass);
+ _mass->set_sensitive(true);
+ }
+
+ _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_separators.back());
+
+ /* Overlap */
+ {
+ _split = add_toggle_button(_("Break apart cut items"),
+ _("Break apart cut items"));
+ _split->set_icon_name(INKSCAPE_ICON("distribute-randomize"));
+ _split->set_active( prefs->getBool("/tools/eraser/break_apart", false) );
+ _split->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::toggle_break_apart));
+ }
+
+ show_all();
+
+ set_eraser_mode_visibility(eraser_mode);
+}
+
+GtkWidget *
+EraserToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new EraserToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+EraserToolbar::mode_changed(int mode)
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt( "/tools/eraser/mode", mode );
+ }
+
+ set_eraser_mode_visibility(mode);
+
+ // only take action if run by the attr_changed listener
+ if (!_freeze) {
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ /*
+ if ( eraser_mode != ERASER_MODE_DELETE ) {
+ } else {
+ }
+ */
+ // TODO finish implementation
+
+ _freeze = false;
+ }
+}
+
+void
+EraserToolbar::set_eraser_mode_visibility(const guint eraser_mode)
+{
+ _split->set_visible((eraser_mode == ERASER_MODE_CUT));
+
+ const gboolean visibility = (eraser_mode != ERASER_MODE_DELETE);
+
+ const std::array<Gtk::Widget *, 6> arr = {_cap_rounding,
+ _mass,
+ _thinning,
+ _tremor,
+ _usepressure,
+ _width};
+ for (auto widget : arr) {
+ widget->set_visible(visibility);
+ }
+
+ for (auto separator : _separators) {
+ separator->set_visible(visibility);
+ }
+}
+
+void
+EraserToolbar::width_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/eraser/width", _width_adj->get_value() );
+}
+
+void
+EraserToolbar::mass_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/eraser/mass", _mass_adj->get_value() );
+}
+
+void
+EraserToolbar::velthin_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/eraser/thinning", _thinning_adj->get_value() );
+}
+
+void
+EraserToolbar::cap_rounding_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/eraser/cap_rounding", _cap_rounding_adj->get_value() );
+}
+
+void
+EraserToolbar::tremor_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/eraser/tremor", _tremor_adj->get_value() );
+}
+
+void
+EraserToolbar::toggle_break_apart()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _split->get_active();
+ prefs->setBool("/tools/eraser/break_apart", active);
+}
+
+void
+EraserToolbar::usepressure_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/eraser/usepressure", _usepressure->get_active());
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/eraser-toolbar.h b/src/ui/toolbar/eraser-toolbar.h
new file mode 100644
index 0000000..4ab94b7
--- /dev/null
+++ b/src/ui/toolbar/eraser-toolbar.h
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_ERASOR_TOOLBAR_H
+#define SEEN_ERASOR_TOOLBAR_H
+
+/**
+ * @file
+ * Erasor aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class SeparatorToolItem;
+}
+
+namespace Inkscape {
+namespace UI {
+class SimplePrefPusher;
+
+namespace Widget {
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class EraserToolbar : public Toolbar {
+private:
+ UI::Widget::SpinButtonToolItem *_width;
+ UI::Widget::SpinButtonToolItem *_mass;
+ UI::Widget::SpinButtonToolItem *_thinning;
+ UI::Widget::SpinButtonToolItem *_cap_rounding;
+ UI::Widget::SpinButtonToolItem *_tremor;
+
+ Gtk::ToggleToolButton *_usepressure;
+ Gtk::ToggleToolButton *_split;
+
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _mass_adj;
+ Glib::RefPtr<Gtk::Adjustment> _thinning_adj;
+ Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj;
+ Glib::RefPtr<Gtk::Adjustment> _tremor_adj;
+
+ std::unique_ptr<SimplePrefPusher> _pressure_pusher;
+
+ std::vector<Gtk::SeparatorToolItem *> _separators;
+
+ bool _freeze;
+
+ void mode_changed(int mode);
+ void set_eraser_mode_visibility(const guint eraser_mode);
+ void width_value_changed();
+ void mass_value_changed();
+ void velthin_value_changed();
+ void cap_rounding_value_changed();
+ void tremor_value_changed();
+ static void update_presets_list(gpointer data);
+ void toggle_break_apart();
+ void usepressure_toggled();
+
+protected:
+ EraserToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_ERASOR_TOOLBAR_H */
diff --git a/src/ui/toolbar/gradient-toolbar.cpp b/src/ui/toolbar/gradient-toolbar.cpp
new file mode 100644
index 0000000..e49dab4
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.cpp
@@ -0,0 +1,1178 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient aux toolbar
+ *
+ * Authors:
+ * bulia byak <bulia@dr.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "gradient-drag.h"
+#include "gradient-toolbar.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-stop.h"
+#include "style.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/gradient-tool.h"
+#include "ui/util.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "widgets/gradient-image.h"
+#include "widgets/gradient-vector.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Tools::ToolBase;
+
+static bool blocked = false;
+
+void gr_apply_gradient_to_item( SPItem *item, SPGradient *gr, SPGradientType initialType, Inkscape::PaintTarget initialMode, Inkscape::PaintTarget mode )
+{
+ SPStyle *style = item->style;
+ bool isFill = (mode == Inkscape::FOR_FILL);
+ if (style
+ && (isFill ? style->fill.isPaintserver() : style->stroke.isPaintserver())
+ //&& SP_IS_GRADIENT(isFill ? style->getFillPaintServer() : style->getStrokePaintServer()) ) {
+ && (isFill ? SP_IS_GRADIENT(style->getFillPaintServer()) : SP_IS_GRADIENT(style->getStrokePaintServer())) ) {
+ SPPaintServer *server = isFill ? style->getFillPaintServer() : style->getStrokePaintServer();
+ if ( SP_IS_LINEARGRADIENT(server) ) {
+ sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_LINEAR, mode);
+ } else if ( SP_IS_RADIALGRADIENT(server) ) {
+ sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_RADIAL, mode);
+ }
+ }
+ else if (initialMode == mode)
+ {
+ sp_item_set_gradient(item, gr, initialType, mode);
+ }
+}
+
+/**
+Applies gradient vector gr to the gradients attached to the selected dragger of drag, or if none,
+to all objects in selection. If there was no previous gradient on an item, uses gradient type and
+fill/stroke setting from preferences to create new default (linear: left/right; radial: centered)
+gradient.
+*/
+void gr_apply_gradient(Inkscape::Selection *selection, GrDrag *drag, SPGradient *gr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ SPGradientType initialType = static_cast<SPGradientType>(prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR));
+ Inkscape::PaintTarget initialMode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+
+ // GRADIENTFIXME: make this work for multiple selected draggers.
+
+ // First try selected dragger
+ if (drag && !drag->selected.empty()) {
+ GrDragger *dragger = *(drag->selected.begin());
+ for(auto draggable : dragger->draggables) { //for all draggables of dragger
+ gr_apply_gradient_to_item(draggable->item, gr, initialType, initialMode, draggable->fill_or_stroke);
+ }
+ return;
+ }
+
+ // If no drag or no dragger selected, act on selection
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ gr_apply_gradient_to_item(*i, gr, initialType, initialMode, initialMode);
+ }
+}
+
+int gr_vector_list(Glib::RefPtr<Gtk::ListStore> store, SPDesktop *desktop,
+ bool selection_empty, SPGradient *gr_selected, bool gr_multi)
+{
+ int selected = -1;
+
+ if (!blocked) {
+ std::cerr << "gr_vector_list: should be blocked!" << std::endl;
+ }
+
+ // Get list of gradients in document.
+ SPDocument *document = desktop->getDocument();
+ std::vector<SPObject *> gl;
+ std::vector<SPObject *> gradients = document->getResourceList( "gradient" );
+ for (auto gradient : gradients) {
+ SPGradient *grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && !grad->isSolid() ) {
+ gl.push_back(gradient);
+ }
+ }
+
+ store->clear();
+
+ Inkscape::UI::Widget::ComboToolItemColumns columns;
+ Gtk::TreeModel::Row row;
+
+ if (gl.empty()) {
+ // The document has no gradients
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else if (selection_empty) {
+ // Document has gradients, but nothing is currently selected.
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Nothing Selected");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else {
+
+ if (gr_selected == nullptr) {
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+ }
+
+ if (gr_multi) {
+ row = *(store->append());
+ row[columns.col_label ] = _("Multiple gradients");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+ }
+
+ int idx = 0;
+ for (auto it : gl) {
+ SPGradient *gradient = SP_GRADIENT(it);
+
+ Glib::ustring label = gr_prepare_label(gradient);
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradient_to_pixbuf_ref(gradient, 64, 16);
+
+ row = *(store->append());
+ row[columns.col_label ] = label;
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_pixbuf ] = pixbuf;
+ row[columns.col_data ] = gradient;
+ row[columns.col_sensitive] = true;
+
+ if (gradient == gr_selected) {
+ selected = idx;
+ }
+ idx ++;
+ }
+
+ if (gr_multi) {
+ selected = 0; // This will show "Multiple Gradients"
+ }
+ }
+
+ return selected;
+}
+
+/*
+ * Get the gradient of the selected desktop item
+ * This is gradient containing the repeat settings, not the underlying "getVector" href linked gradient.
+ */
+void gr_get_dt_selected_gradient(Inkscape::Selection *selection, SPGradient *&gr_selected)
+{
+ SPGradient *gradient = nullptr;
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;// get the items gradient, not the getVector() version
+ SPStyle *style = item->style;
+ SPPaintServer *server = nullptr;
+
+ if (style && (style->fill.isPaintserver())) {
+ server = item->style->getFillPaintServer();
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ server = item->style->getStrokePaintServer();
+ }
+
+ if ( SP_IS_GRADIENT(server) ) {
+ gradient = SP_GRADIENT(server);
+ }
+ }
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient) {
+ gr_selected = gradient;
+ }
+}
+
+/*
+ * Get the current selection and dragger status from the desktop
+ */
+void gr_read_selection( Inkscape::Selection *selection,
+ GrDrag *drag,
+ SPGradient *&gr_selected,
+ bool &gr_multi,
+ SPGradientSpread &spr_selected,
+ bool &spr_multi )
+{
+ if (drag && !drag->selected.empty()) {
+ // GRADIENTFIXME: make this work for more than one selected dragger?
+ GrDragger *dragger = *(drag->selected.begin());
+ for(auto draggable : dragger->draggables) { //for all draggables of dragger
+ SPGradient *gradient = sp_item_gradient_get_vector(draggable->item, draggable->fill_or_stroke);
+ SPGradientSpread spread = sp_item_gradient_get_spread(draggable->item, draggable->fill_or_stroke);
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ return;
+ }
+
+ // If no selected dragger, read desktop selection
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ SPStyle *style = item->style;
+
+ if (style && (style->fill.isPaintserver())) {
+ SPPaintServer *server = item->style->getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient *gradient = SP_GRADIENT(server)->getVector();
+ SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread();
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient *gradient = SP_GRADIENT(server)->getVector();
+ SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread();
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ }
+ }
+ }
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+GradientToolbar::GradientToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ /* New gradient linear or radial */
+ {
+ add_label(_("New:"));
+
+ Gtk::RadioToolButton::Group new_type_group;
+
+ auto linear_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("linear")));
+ linear_button->set_tooltip_text(_("Create linear gradient"));
+ linear_button->set_icon_name(INKSCAPE_ICON("paint-gradient-linear"));
+ _new_type_buttons.push_back(linear_button);
+
+ auto radial_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("radial")));
+ radial_button->set_tooltip_text(_("Create radial (elliptic or circular) gradient"));
+ radial_button->set_icon_name(INKSCAPE_ICON("paint-gradient-radial"));
+ _new_type_buttons.push_back(radial_button);
+
+ gint mode = prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR);
+ _new_type_buttons[ mode == SP_GRADIENT_TYPE_LINEAR ? 0 : 1 ]->set_active(); // linear == 1, radial == 2
+
+ int btn_index = 0;
+ for (auto btn : _new_type_buttons)
+ {
+ btn->set_sensitive(true);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_type_changed), btn_index++));
+ add(*btn);
+ }
+ }
+
+ /* New gradient on fill or stroke*/
+ {
+ Gtk::RadioToolButton::Group new_fillstroke_group;
+
+ auto fill_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill")));
+ fill_btn->set_tooltip_text(_("Create gradient in the fill"));
+ fill_btn->set_icon_name(INKSCAPE_ICON("object-fill"));
+ _new_fillstroke_buttons.push_back(fill_btn);
+
+ auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke")));
+ stroke_btn->set_tooltip_text(_("Create gradient in the stroke"));
+ stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke"));
+ _new_fillstroke_buttons.push_back(stroke_btn);
+
+ auto fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ _new_fillstroke_buttons[ fsmode == Inkscape::FOR_FILL ? 0 : 1 ]->set_active();
+
+ auto btn_index = 0;
+ for (auto btn : _new_fillstroke_buttons)
+ {
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_fillstroke_changed), btn_index++));
+ btn->set_sensitive();
+ add(*btn);
+ }
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Gradient Select list*/
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ auto store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ _select_cb = UI::Widget::ComboToolItem::create(_("Select"), // Label
+ "", // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+
+ _select_cb->use_icon( false );
+ _select_cb->use_pixbuf( true );
+ _select_cb->use_group_label( true );
+ _select_cb->set_active( 0 );
+ _select_cb->set_sensitive( false );
+
+ add(*_select_cb);
+ _select_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::gradient_changed));
+ }
+
+ // Gradients Linked toggle
+ {
+ _linked_item = add_toggle_button(_("Link gradients"),
+ _("Link gradients to change all related gradients"));
+ _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked"));
+ _linked_item->signal_toggled().connect(sigc::mem_fun(*this, &GradientToolbar::linked_changed));
+
+ bool linkedmode = prefs->getBool("/options/forkgradientvectors/value", true);
+ _linked_item->set_active(!linkedmode);
+ }
+
+ /* Reverse */
+ {
+ _stops_reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse")));
+ _stops_reverse_item->set_tooltip_text(_("Reverse the direction of the gradient"));
+ _stops_reverse_item->set_icon_name(INKSCAPE_ICON("object-flip-horizontal"));
+ _stops_reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::reverse));
+ add(*_stops_reverse_item);
+ _stops_reverse_item->set_sensitive(false);
+ }
+
+ // Gradient Spread type (how a gradient is drawn outside its nominal area)
+ {
+ UI::Widget::ComboToolItemColumns columns;
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ std::vector<gchar*> spread_dropdown_items_list = {
+ const_cast<gchar *>(C_("Gradient repeat type", "None")),
+ _("Reflected"),
+ _("Direct")
+ };
+
+ for (auto item: spread_dropdown_items_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _spread_cb = Gtk::manage(UI::Widget::ComboToolItem::create(_("Repeat: "),
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/pservers.html#LinearGradientSpreadMethodAttribute
+ _("Whether to fill with flat color beyond the ends of the gradient vector "
+ "(spreadMethod=\"pad\"), or repeat the gradient in the same direction "
+ "(spreadMethod=\"repeat\"), or repeat the gradient in alternating opposite "
+ "directions (spreadMethod=\"reflect\")"),
+ "Not Used", store));
+ _spread_cb->use_group_label(true);
+
+ _spread_cb->set_active(0);
+ _spread_cb->set_sensitive(false);
+
+ _spread_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::spread_changed));
+ add(*_spread_cb);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Gradient Stop list */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ auto store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No stops");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ _stop_cb =
+ UI::Widget::ComboToolItem::create(_("Stops" ), // Label
+ "", // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+
+ _stop_cb->use_icon( false );
+ _stop_cb->use_pixbuf( true );
+ _stop_cb->use_group_label( true );
+ _stop_cb->set_active( 0 );
+ _stop_cb->set_sensitive( false );
+
+ add(*_stop_cb);
+ _stop_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_changed));
+ }
+
+ /* Offset */
+ {
+ auto offset_val = prefs->getDouble("/tools/gradient/stopoffset", 0);
+ _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 1.0, 0.01, 0.1);
+ _offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("gradient-stopoffset", C_("Gradient", "Offset:"), _offset_adj, 0.01, 2));
+ _offset_item->set_tooltip_text(_("Offset of selected stop"));
+ _offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_offset_adjustment_changed));
+ add(*_offset_item);
+ _offset_item->set_sensitive(false);
+ }
+
+ /* Add stop */
+ {
+ _stops_add_item = Gtk::manage(new Gtk::ToolButton(_("Insert new stop")));
+ _stops_add_item->set_tooltip_text(_("Insert new stop"));
+ _stops_add_item->set_icon_name(INKSCAPE_ICON("node-add"));
+ _stops_add_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::add_stop));
+ add(*_stops_add_item);
+ _stops_add_item->set_sensitive(false);
+ }
+
+ /* Delete stop */
+ {
+ _stops_delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete stop")));
+ _stops_delete_item->set_tooltip_text(_("Delete stop"));
+ _stops_delete_item->set_icon_name(INKSCAPE_ICON("node-delete"));
+ _stops_delete_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::remove_stop));
+ add(*_stops_delete_item);
+ _stops_delete_item->set_sensitive(false);
+ }
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &GradientToolbar::check_ec));
+
+ show_all();
+}
+
+/**
+ * Gradient auxiliary toolbar construction and setup.
+ *
+ */
+GtkWidget *
+GradientToolbar::create(SPDesktop * desktop)
+{
+ auto toolbar = new GradientToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+GradientToolbar::new_type_changed(int mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/gradient/newgradient",
+ mode == 0 ? SP_GRADIENT_TYPE_LINEAR : SP_GRADIENT_TYPE_RADIAL);
+}
+
+void
+GradientToolbar::new_fillstroke_changed(int mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Inkscape::PaintTarget fsmode = (mode == 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ prefs->setInt("/tools/gradient/newfillorstroke", (fsmode == Inkscape::FOR_FILL) ? 1 : 0);
+}
+
+/*
+ * User selected a gradient from the combobox
+ */
+void
+GradientToolbar::gradient_changed(int active)
+{
+ if (blocked) {
+ return;
+ }
+
+ if (active < 0) {
+ return;
+ }
+
+ blocked = true;
+
+ SPGradient *gr = get_selected_gradient();
+
+ if (gr) {
+ gr = sp_gradient_ensure_vector_normalized(gr);
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ ToolBase *ev = _desktop->getEventContext();
+
+ gr_apply_gradient(selection, ev ? ev->get_drag() : nullptr, gr);
+
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT,
+ _("Assign gradient to object"));
+ }
+
+ blocked = false;
+}
+
+/**
+ * \brief Return gradient selected in menu
+ */
+SPGradient *
+GradientToolbar::get_selected_gradient()
+{
+ int active = _select_cb->get_active();
+
+ auto store = _select_cb->get_store();
+ auto row = store->children()[active];
+ UI::Widget::ComboToolItemColumns columns;
+
+ void* pointer = row[columns.col_data];
+ SPGradient *gr = static_cast<SPGradient *>(pointer);
+
+ return gr;
+}
+
+/**
+ * \brief User selected a spread method from the combobox
+ */
+void
+GradientToolbar::spread_changed(int active)
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = true;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ SPGradient *gradient = nullptr;
+ gr_get_dt_selected_gradient(selection, gradient);
+
+ if (gradient) {
+ SPGradientSpread spread = (SPGradientSpread) active;
+ gradient->setSpread(spread);
+ gradient->updateRepr();
+
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT,
+ _("Set gradient repeat"));
+ }
+
+ blocked = false;
+}
+
+/**
+ * \brief User selected a stop from the combobox
+ */
+void
+GradientToolbar::stop_changed(int active)
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = true;
+
+ ToolBase *ev = _desktop->getEventContext();
+ SPGradient *gr = get_selected_gradient();
+
+ select_dragger_by_stop(gr, ev);
+
+ blocked = false;
+}
+
+void
+GradientToolbar::select_dragger_by_stop(SPGradient *gradient,
+ ToolBase *ev)
+{
+ if (!blocked) {
+ std::cerr << "select_dragger_by_stop: should be blocked!" << std::endl;
+ }
+
+ if (!ev || !gradient) {
+ return;
+ }
+
+ GrDrag *drag = ev->get_drag();
+ if (!drag) {
+ return;
+ }
+
+ SPStop *stop = get_selected_stop();
+
+ drag->selectByStop(stop, false, true);
+
+ stop_set_offset();
+}
+
+/**
+ * \brief Get stop selected by menu
+ */
+SPStop *
+GradientToolbar::get_selected_stop()
+{
+ int active = _stop_cb->get_active();
+
+ auto store = _stop_cb->get_store();
+ auto row = store->children()[active];
+ UI::Widget::ComboToolItemColumns columns;
+ void* pointer = row[columns.col_data];
+ SPStop *stop = static_cast<SPStop *>(pointer);
+
+ return stop;
+}
+
+/**
+ * Change desktop dragger selection to this stop
+ *
+ * Set the offset widget value (based on which stop is selected)
+ */
+void
+GradientToolbar::stop_set_offset()
+{
+ if (!blocked) {
+ std::cerr << "gr_stop_set_offset: should be blocked!" << std::endl;
+ }
+
+ SPStop *stop = get_selected_stop();
+ if (!stop) {
+ // std::cerr << "gr_stop_set_offset: no stop!" << std::endl;
+ return;
+ }
+
+ if (!_offset_item) {
+ return;
+ }
+ bool isEndStop = false;
+
+ SPStop *prev = nullptr;
+ prev = stop->getPrevStop();
+ if (prev != nullptr ) {
+ _offset_adj->set_lower(prev->offset);
+ } else {
+ isEndStop = true;
+ _offset_adj->set_lower(0);
+ }
+
+ SPStop *next = nullptr;
+ next = stop->getNextStop();
+ if (next != nullptr ) {
+ _offset_adj->set_upper(next->offset);
+ } else {
+ isEndStop = true;
+ _offset_adj->set_upper(1.0);
+ }
+
+ _offset_adj->set_value(stop->offset);
+ _offset_item->set_sensitive( !isEndStop );
+}
+
+/**
+ * \brief User changed the offset
+ */
+void
+GradientToolbar::stop_offset_adjustment_changed()
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = true;
+
+ SPStop *stop = get_selected_stop();
+ if (stop) {
+ stop->offset = _offset_adj->get_value();
+ sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset);
+
+ DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", SP_VERB_CONTEXT_GRADIENT,
+ _("Change gradient stop offset"));
+
+ }
+
+ blocked = false;
+}
+
+/**
+ * \brief Add stop to gradient
+ */
+void
+GradientToolbar::add_stop()
+{
+ if (!_desktop) {
+ return;
+ }
+
+ auto selection = _desktop->getSelection();
+ if (!selection) {
+ return;
+ }
+
+ auto ev = _desktop->getEventContext();
+ auto rc = SP_GRADIENT_CONTEXT(ev);
+
+ if (rc) {
+ sp_gradient_context_add_stops_between_selected_stops(rc);
+ }
+}
+
+/**
+ * \brief Remove stop from vector
+ */
+void
+GradientToolbar::remove_stop()
+{
+ if (!_desktop) {
+ return;
+ }
+
+ auto selection = _desktop->getSelection(); // take from desktop, not from args
+ if (!selection) {
+ return;
+ }
+
+ auto ev = _desktop->getEventContext();
+ GrDrag *drag = nullptr;
+ if (ev) {
+ drag = ev->get_drag();
+ }
+
+ if (drag) {
+ drag->deleteSelected();
+ }
+}
+
+/**
+ * \brief Reverse vector
+ */
+void
+GradientToolbar::reverse()
+{
+ sp_gradient_reverse_selected_gradients(_desktop);
+}
+
+/**
+ * \brief Lock or unlock links
+ */
+void
+GradientToolbar::linked_changed()
+{
+ bool active = _linked_item->get_active();
+ if ( active ) {
+ _linked_item->set_icon_name(INKSCAPE_ICON("object-locked"));
+ } else {
+ _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked"));
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/forkgradientvectors/value", !active);
+}
+
+// lp:1327267
+/**
+ * Checks the current tool and connects gradient aux toolbox signals if it happens to be the gradient tool.
+ * Called every time the current tool changes by signal emission.
+ */
+void
+GradientToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (SP_IS_GRADIENT_CONTEXT(ec)) {
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+
+ // connect to selection modified and changed signals
+ _connection_changed = selection->connectChanged(sigc::mem_fun(*this, &GradientToolbar::selection_changed));
+ _connection_modified = selection->connectModified(sigc::mem_fun(*this, &GradientToolbar::selection_modified));
+ _connection_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &GradientToolbar::drag_selection_changed));
+
+ // Is this necessary? Couldn't hurt.
+ selection_changed(selection);
+
+ // connect to release and modified signals of the defs (i.e. when someone changes gradient)
+ _connection_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &GradientToolbar::defs_release));
+ _connection_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &GradientToolbar::defs_modified));
+ } else {
+ if (_connection_changed)
+ _connection_changed.disconnect();
+ if (_connection_modified)
+ _connection_modified.disconnect();
+ if (_connection_subselection_changed)
+ _connection_subselection_changed.disconnect();
+ if (_connection_defs_release)
+ _connection_defs_release.disconnect();
+ if (_connection_defs_modified)
+ _connection_defs_modified.disconnect();
+ }
+}
+
+/**
+ * Core function, setup all the widgets whenever something changes on the desktop
+ */
+void
+GradientToolbar::selection_changed(Inkscape::Selection * /*selection*/)
+{
+ if (blocked)
+ return;
+
+ blocked = true;
+
+ if (!_desktop) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args
+ if (selection) {
+
+ ToolBase *ev = _desktop->getEventContext();
+ GrDrag *drag = nullptr;
+ if (ev) {
+ drag = ev->get_drag();
+ }
+
+ SPGradient *gr_selected = nullptr;
+ SPGradientSpread spr_selected = SP_GRADIENT_SPREAD_UNDEFINED;
+ bool gr_multi = false;
+ bool spr_multi = false;
+
+ gr_read_selection(selection, drag, gr_selected, gr_multi, spr_selected, spr_multi);
+
+ // Gradient selection menu
+ auto store = _select_cb->get_store();
+ int gradient = gr_vector_list (store, _desktop, selection->isEmpty(), gr_selected, gr_multi);
+
+ if (gradient < 0) {
+ // No selection or no gradients
+ _select_cb->set_active( 0 );
+ _select_cb->set_sensitive (false);
+ } else {
+ // Single gradient or multiple gradients
+ _select_cb->set_active( gradient );
+ _select_cb->set_sensitive (true);
+ }
+
+ // Spread menu
+ _spread_cb->set_sensitive( gr_selected && !gr_multi );
+ _spread_cb->set_active( gr_selected ? (int)spr_selected : 0 );
+
+ _stops_add_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty()));
+ _stops_delete_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty()));
+ _stops_reverse_item->set_sensitive((gr_selected!= nullptr));
+
+ _stop_cb->set_sensitive( gr_selected && !gr_multi);
+
+ update_stop_list (gr_selected, nullptr, gr_multi);
+ select_stop_by_draggers(gr_selected, ev);
+ }
+
+ blocked = false;
+}
+
+/**
+ * \brief Construct stop list
+ */
+int
+GradientToolbar::update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi)
+{
+ if (!blocked) {
+ std::cerr << "update_stop_list should be blocked!" << std::endl;
+ }
+
+ int selected = -1;
+
+ auto store = _stop_cb->get_store();
+
+ if (!store) {
+ return selected;
+ }
+
+ store->clear();
+
+ UI::Widget::ComboToolItemColumns columns;
+ Gtk::TreeModel::Row row;
+
+ if (!SP_IS_GRADIENT(gradient)) {
+ // No valid gradient
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else if (!gradient->hasStops()) {
+ // Has gradient but it has no stops
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No stops in gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else {
+ // Gradient has stops
+
+ // Get list of stops
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+
+ SPStop *stop = SP_STOP(&ochild);
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradstop_to_pixbuf_ref (stop, 32, 16);
+
+ Inkscape::XML::Node *repr = reinterpret_cast<SPItem *>(&ochild)->getRepr();
+ Glib::ustring label = gr_ellipsize_text(repr->attribute("id"), 25);
+
+ row = *(store->append());
+ row[columns.col_label ] = label;
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_pixbuf ] = pixbuf;
+ row[columns.col_data ] = stop;
+ row[columns.col_sensitive] = true;
+ }
+ }
+ }
+
+ if (new_stop != nullptr) {
+ selected = select_stop_in_list (gradient, new_stop);
+ }
+
+ return selected;
+}
+
+/**
+ * \brief Find position of new_stop in menu.
+ */
+int
+GradientToolbar::select_stop_in_list(SPGradient *gradient, SPStop *new_stop)
+{
+ int i = 0;
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+ if (&ochild == new_stop) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+}
+
+/**
+ * \brief Set stop in menu to match stops selected by draggers
+ */
+void
+GradientToolbar::select_stop_by_draggers(SPGradient *gradient, ToolBase *ev)
+{
+ if (!blocked) {
+ std::cerr << "select_stop_by_draggers should be blocked!" << std::endl;
+ }
+
+ if (!ev || !gradient)
+ return;
+
+ SPGradient *vector = gradient->getVector();
+ if (!vector)
+ return;
+
+ GrDrag *drag = ev->get_drag();
+
+ if (!drag || drag->selected.empty()) {
+ _stop_cb->set_active(0);
+ stop_set_offset();
+ return;
+ }
+
+ gint n = 0;
+ SPStop *stop = nullptr;
+ int selected = -1;
+
+ // For all selected draggers
+ for(auto dragger : drag->selected) {
+
+ // For all draggables of dragger
+ for(auto draggable : dragger->draggables) {
+
+ if (draggable->point_type != POINT_RG_FOCUS) {
+ n++;
+ if (n > 1) break;
+ }
+
+ stop = vector->getFirstStop();
+
+ switch (draggable->point_type) {
+ case POINT_LG_MID:
+ case POINT_RG_MID1:
+ case POINT_RG_MID2:
+ stop = sp_get_stop_i(vector, draggable->point_i);
+ break;
+ case POINT_LG_END:
+ case POINT_RG_R1:
+ case POINT_RG_R2:
+ stop = sp_last_stop(vector);
+ break;
+ default:
+ break;
+ }
+ }
+ if (n > 1) break;
+ }
+
+ if (n > 1) {
+ // Multiple stops selected
+ if (_offset_item) {
+ _offset_item->set_sensitive(false);
+ }
+
+ // Stop list always updated first... reinsert "Multiple stops" as first entry.
+ UI::Widget::ComboToolItemColumns columns;
+ auto store = _stop_cb->get_store();
+
+ auto row = *(store->prepend());
+ row[columns.col_label ] = _("Multiple stops");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+ selected = 0;
+
+ } else {
+ selected = select_stop_in_list(gradient, stop);
+ }
+
+ if (selected < 0) {
+ _stop_cb->set_active (0);
+ _stop_cb->set_sensitive (false);
+ } else {
+ _stop_cb->set_active (selected);
+ _stop_cb->set_sensitive (true);
+ stop_set_offset();
+ }
+}
+
+void
+GradientToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ selection_changed(selection);
+}
+
+void
+GradientToolbar::drag_selection_changed(gpointer /*dragger*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+GradientToolbar::defs_release(SPObject * /*defs*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+GradientToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/)
+{
+ selection_changed(nullptr);
+}
+
+}
+}
+}
+/*
+ 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/toolbar/gradient-toolbar.h b/src/ui/toolbar/gradient-toolbar.h
new file mode 100644
index 0000000..d5ea1b0
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_TOOLBAR_H
+#define SEEN_GRADIENT_TOOLBAR_H
+
+/*
+ * Gradient aux toolbar
+ *
+ * Authors:
+ * bulia byak <bulia@dr.com>
+ *
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPGradient;
+
+namespace Gtk {
+class ComboBoxText;
+class ToolButton;
+class ToolItem;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class ComboToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class GradientToolbar : public Toolbar {
+private:
+ std::vector<Gtk::RadioToolButton *> _new_type_buttons;
+ std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons;
+ UI::Widget::ComboToolItem *_select_cb;
+ UI::Widget::ComboToolItem *_spread_cb;
+ UI::Widget::ComboToolItem *_stop_cb;
+
+ Gtk::ToolButton *_stops_add_item;
+ Gtk::ToolButton *_stops_delete_item;
+ Gtk::ToolButton *_stops_reverse_item;
+ Gtk::ToggleToolButton *_linked_item;
+
+ UI::Widget::SpinButtonToolItem *_offset_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _offset_adj;
+
+ void new_type_changed(int mode);
+ void new_fillstroke_changed(int mode);
+ void gradient_changed(int active);
+ SPGradient * get_selected_gradient();
+ void spread_changed(int active);
+ void stop_changed(int active);
+ void select_dragger_by_stop(SPGradient *gradient,
+ UI::Tools::ToolBase *ev);
+ SPStop * get_selected_stop();
+ void stop_set_offset();
+ void stop_offset_adjustment_changed();
+ void add_stop();
+ void remove_stop();
+ void reverse();
+ void linked_changed();
+ void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+ int update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi);
+ int select_stop_in_list(SPGradient *gradient, SPStop *new_stop);
+ void select_stop_by_draggers(SPGradient *gradient, UI::Tools::ToolBase *ev);
+ void selection_modified(Inkscape::Selection *selection, guint flags);
+ void drag_selection_changed(gpointer dragger);
+ void defs_release(SPObject * defs);
+ void defs_modified(SPObject *defs, guint flags);
+
+ sigc::connection _connection_changed;
+ sigc::connection _connection_modified;
+ sigc::connection _connection_subselection_changed;
+ sigc::connection _connection_defs_release;
+ sigc::connection _connection_defs_modified;
+
+protected:
+ GradientToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_GRADIENT_TOOLBAR_H */
diff --git a/src/ui/toolbar/lpe-toolbar.cpp b/src/ui/toolbar/lpe-toolbar.cpp
new file mode 100644
index 0000000..ad08f6a
--- /dev/null
+++ b/src/ui/toolbar/lpe-toolbar.cpp
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * LPE aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "lpe-toolbar.h"
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "live_effects/lpe-line_segment.h"
+
+#include "helper/action-context.h"
+#include "helper/action.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Tools::ToolBase;
+using Inkscape::UI::Tools::LpeTool;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+LPEToolbar::LPEToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Util::UNIT_TYPE_LINEAR)),
+ _freeze(false),
+ _currentlpe(nullptr),
+ _currentlpeitem(nullptr)
+{
+ _tracker->setActiveUnit(_desktop->getNamedView()->display_units);
+
+ auto unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setString("/tools/lpetool/unit", unit->abbr);
+
+ /* Automatically create a list of LPEs that get added to the toolbar **/
+ {
+ Gtk::RadioToolButton::Group mode_group;
+
+ // The first toggle button represents the state that no subtool is active.
+ auto inactive_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("All inactive")));
+ inactive_mode_btn->set_tooltip_text(_("No geometric tool is active"));
+ inactive_mode_btn->set_icon_name(INKSCAPE_ICON("draw-geometry-inactive"));
+ _mode_buttons.push_back(inactive_mode_btn);
+
+ Inkscape::LivePathEffect::EffectType type;
+ for (int i = 1; i < num_subtools; ++i) { // i == 0 ia INVALIDE_LPE.
+
+ type = lpesubtools[i].type;
+
+ auto btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, Inkscape::LivePathEffect::LPETypeConverter.get_label(type)));
+ btn->set_tooltip_text(_(Inkscape::LivePathEffect::LPETypeConverter.get_label(type).c_str()));
+ btn->set_icon_name(lpesubtools[i].icon_name);
+ _mode_buttons.push_back(btn);
+ }
+
+ int btn_idx = 0;
+ for (auto btn : _mode_buttons) {
+ btn->set_sensitive(true);
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &LPEToolbar::mode_changed), btn_idx++));
+ }
+
+ int mode = prefs->getInt("/tools/lpetool/mode", 0);
+ _mode_buttons[mode]->set_active();
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Show limiting bounding box */
+ {
+ _show_bbox_item = add_toggle_button(_("Show limiting bounding box"),
+ _("Show bounding box (used to cut infinite lines)"));
+ _show_bbox_item->set_icon_name(INKSCAPE_ICON("show-bounding-box"));
+ _show_bbox_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_bbox));
+ _show_bbox_item->set_active(prefs->getBool( "/tools/lpetool/show_bbox", true ));
+ }
+
+ /* Set limiting bounding box to bbox of current selection */
+ {
+ // TODO: Shouldn't this just be a button (not toggle button)?
+ _bbox_from_selection_item = add_toggle_button(_("Get limiting bounding box from selection"),
+ _("Set limiting bounding box (used to cut infinite lines) to the bounding box of current selection"));
+ _bbox_from_selection_item->set_icon_name(INKSCAPE_ICON("draw-geometry-set-bounding-box"));
+ _bbox_from_selection_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_set_bbox));
+ _bbox_from_selection_item->set_active(false);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Combo box to choose line segment type */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ std::vector<gchar*> line_segment_dropdown_items_list = {
+ _("Closed"),
+ _("Open start"),
+ _("Open end"),
+ _("Open both")
+ };
+
+ for (auto item: line_segment_dropdown_items_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _line_segment_combo = Gtk::manage(UI::Widget::ComboToolItem::create(_("Line Type"), _("Choose a line segment type"), "Not Used", store));
+ _line_segment_combo->use_group_label(false);
+
+ _line_segment_combo->set_active(0);
+
+ _line_segment_combo->signal_changed().connect(sigc::mem_fun(*this, &LPEToolbar::change_line_segment_type));
+ add(*_line_segment_combo);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Display measuring info for selected items */
+ {
+ _measuring_item = add_toggle_button(_("Display measuring info"),
+ _("Display measuring info for selected items"));
+ _measuring_item->set_icon_name(INKSCAPE_ICON("draw-geometry-show-measuring-info"));
+ _measuring_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_measuring_info));
+ _measuring_item->set_active( prefs->getBool( "/tools/lpetool/show_measuring_info", true ) );
+ }
+
+ // Add the units menu
+ {
+ _units_item = _tracker->create_tool_item(_("Units"), ("") );
+ add(*_units_item);
+ _units_item->signal_changed_after().connect(sigc::mem_fun(*this, &LPEToolbar::unit_changed));
+ _units_item->set_sensitive( prefs->getBool("/tools/lpetool/show_measuring_info", true));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Open LPE dialog (to adapt parameters numerically) */
+ {
+ // TODO: Shouldn't this be a regular Gtk::ToolButton (not toggle)?
+ _open_lpe_dialog_item = add_toggle_button(_("Open LPE dialog"),
+ _("Open LPE dialog (to adapt parameters numerically)"));
+ _open_lpe_dialog_item->set_icon_name(INKSCAPE_ICON("dialog-geometry"));
+ _open_lpe_dialog_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::open_lpe_dialog));
+ _open_lpe_dialog_item->set_active(false);
+ }
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &LPEToolbar::watch_ec));
+
+ show_all();
+}
+
+void
+LPEToolbar::set_mode(int mode)
+{
+ _mode_buttons[mode]->set_active();
+}
+
+GtkWidget *
+LPEToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new LPEToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+// this is called when the mode is changed via the toolbar (i.e., one of the subtool buttons is pressed)
+void
+LPEToolbar::mode_changed(int mode)
+{
+ using namespace Inkscape::LivePathEffect;
+
+ ToolBase *ec = _desktop->event_context;
+ if (!SP_IS_LPETOOL_CONTEXT(ec)) {
+ return;
+ }
+
+ // only take action if run by the attr_changed listener
+ if (!_freeze) {
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ EffectType type = lpesubtools[mode].type;
+
+ LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context);
+ bool success = lpetool_try_construction(lc, type);
+ if (success) {
+ // since the construction was already performed, we set the state back to inactive
+ _mode_buttons[0]->set_active();
+ mode = 0;
+ } else {
+ // switch to the chosen subtool
+ SP_LPETOOL_CONTEXT(_desktop->event_context)->mode = type;
+ }
+
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt( "/tools/lpetool/mode", mode );
+ }
+
+ _freeze = false;
+ }
+}
+
+void
+LPEToolbar::toggle_show_bbox() {
+ auto prefs = Inkscape::Preferences::get();
+
+ bool show = _show_bbox_item->get_active();
+ prefs->setBool("/tools/lpetool/show_bbox", show);
+
+ if (tools_isactive(_desktop, TOOLS_LPETOOL)) {
+ LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context);
+ lpetool_context_reset_limiting_bbox(lc);
+ }
+}
+
+void
+LPEToolbar::toggle_set_bbox()
+{
+ auto selection = _desktop->selection;
+
+ auto bbox = selection->visualBounds();
+
+ if (bbox) {
+ Geom::Point A(bbox->min());
+ Geom::Point B(bbox->max());
+
+ A *= _desktop->doc2dt();
+ B *= _desktop->doc2dt();
+
+ // TODO: should we provide a way to store points in prefs?
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/lpetool/bbox_upperleftx", A[Geom::X]);
+ prefs->setDouble("/tools/lpetool/bbox_upperlefty", A[Geom::Y]);
+ prefs->setDouble("/tools/lpetool/bbox_lowerrightx", B[Geom::X]);
+ prefs->setDouble("/tools/lpetool/bbox_lowerrighty", B[Geom::Y]);
+
+ lpetool_context_reset_limiting_bbox(SP_LPETOOL_CONTEXT(_desktop->event_context));
+ }
+
+ _bbox_from_selection_item->set_active(false);
+}
+
+void
+LPEToolbar::change_line_segment_type(int mode)
+{
+ using namespace Inkscape::LivePathEffect;
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+ auto line_seg = dynamic_cast<LPELineSegment *>(_currentlpe);
+
+ if (_currentlpeitem && line_seg) {
+ line_seg->end_type.param_set_value(static_cast<Inkscape::LivePathEffect::EndType>(mode));
+ sp_lpe_item_update_patheffect(_currentlpeitem, true, true);
+ }
+
+ _freeze = false;
+}
+
+void
+LPEToolbar::toggle_show_measuring_info()
+{
+ if (!tools_isactive(_desktop, TOOLS_LPETOOL)) {
+ return;
+ }
+
+ bool show = _measuring_item->get_active();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/lpetool/show_measuring_info", show);
+
+ LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context);
+ lpetool_show_measuring_info(lc, show);
+
+ _units_item->set_sensitive( show );
+}
+
+void
+LPEToolbar::unit_changed(int /* NotUsed */)
+{
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/tools/lpetool/unit", unit->abbr);
+
+ if (SP_IS_LPETOOL_CONTEXT(_desktop->event_context)) {
+ LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context);
+ lpetool_delete_measuring_items(lc);
+ lpetool_create_measuring_items(lc);
+ }
+}
+
+void
+LPEToolbar::open_lpe_dialog()
+{
+ if (tools_isactive(_desktop, TOOLS_LPETOOL)) {
+ sp_action_perform(Inkscape::Verb::get(SP_VERB_DIALOG_LIVE_PATH_EFFECT)->get_action(Inkscape::ActionContext(_desktop)), nullptr);
+ }
+ _open_lpe_dialog_item->set_active(false);
+}
+
+void
+LPEToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (SP_IS_LPETOOL_CONTEXT(ec)) {
+ // Watch selection
+ c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &LPEToolbar::sel_modified));
+ c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &LPEToolbar::sel_changed));
+ sel_changed(desktop->getSelection());
+ } else {
+ if (c_selection_modified)
+ c_selection_modified.disconnect();
+ if (c_selection_changed)
+ c_selection_changed.disconnect();
+ }
+}
+
+void
+LPEToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ ToolBase *ec = selection->desktop()->event_context;
+ if (SP_IS_LPETOOL_CONTEXT(ec)) {
+ lpetool_update_measuring_items(SP_LPETOOL_CONTEXT(ec));
+ }
+}
+
+void
+LPEToolbar::sel_changed(Inkscape::Selection *selection)
+{
+ using namespace Inkscape::LivePathEffect;
+ ToolBase *ec = selection->desktop()->event_context;
+ if (!SP_IS_LPETOOL_CONTEXT(ec)) {
+ return;
+ }
+ LpeTool *lc = SP_LPETOOL_CONTEXT(ec);
+
+ lpetool_delete_measuring_items(lc);
+ lpetool_create_measuring_items(lc, selection);
+
+ // activate line segment combo box if a single item with LPELineSegment is selected
+ SPItem *item = selection->singleItem();
+ if (item && SP_IS_LPE_ITEM(item) && lpetool_item_has_construction(lc, item)) {
+
+ SPLPEItem *lpeitem = SP_LPE_ITEM(item);
+ Effect* lpe = lpeitem->getCurrentLPE();
+ if (lpe && lpe->effectType() == LINE_SEGMENT) {
+ LPELineSegment *lpels = static_cast<LPELineSegment*>(lpe);
+ _currentlpe = lpe;
+ _currentlpeitem = lpeitem;
+ _line_segment_combo->set_sensitive(true);
+ _line_segment_combo->set_active( lpels->end_type.get_value() );
+ } else {
+ _currentlpe = nullptr;
+ _currentlpeitem = nullptr;
+ _line_segment_combo->set_sensitive(false);
+ }
+
+ } else {
+ _currentlpe = nullptr;
+ _currentlpeitem = nullptr;
+ _line_segment_combo->set_sensitive(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 :
diff --git a/src/ui/toolbar/lpe-toolbar.h b/src/ui/toolbar/lpe-toolbar.h
new file mode 100644
index 0000000..903d9da
--- /dev/null
+++ b/src/ui/toolbar/lpe-toolbar.h
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_LPE_TOOLBAR_H
+#define SEEN_LPE_TOOLBAR_H
+
+/**
+ * @file
+ * LPE aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+class SPDesktop;
+class SPLPEItem;
+
+namespace Gtk {
+class RadioToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace LivePathEffect {
+class Effect;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class ComboToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class LPEToolbar : public Toolbar {
+private:
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+ Gtk::ToggleToolButton *_show_bbox_item;
+ Gtk::ToggleToolButton *_bbox_from_selection_item;
+ Gtk::ToggleToolButton *_measuring_item;
+ Gtk::ToggleToolButton *_open_lpe_dialog_item;
+ UI::Widget::ComboToolItem *_line_segment_combo;
+ UI::Widget::ComboToolItem *_units_item;
+
+ bool _freeze;
+
+ LivePathEffect::Effect *_currentlpe;
+ SPLPEItem *_currentlpeitem;
+
+ sigc::connection c_selection_modified;
+ sigc::connection c_selection_changed;
+
+ void mode_changed(int mode);
+ void unit_changed(int not_used);
+ void sel_modified(Inkscape::Selection *selection, guint flags);
+ void sel_changed(Inkscape::Selection *selection);
+ void change_line_segment_type(int mode);
+ void watch_ec(SPDesktop* desktop, UI::Tools::ToolBase* ec);
+
+ void toggle_show_bbox();
+ void toggle_set_bbox();
+ void toggle_show_measuring_info();
+ void open_lpe_dialog();
+
+protected:
+ LPEToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ void set_mode(int mode);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_LPE_TOOLBAR_H */
diff --git a/src/ui/toolbar/measure-toolbar.cpp b/src/ui/toolbar/measure-toolbar.cpp
new file mode 100644
index 0000000..811a47c
--- /dev/null
+++ b/src/ui/toolbar/measure-toolbar.cpp
@@ -0,0 +1,452 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Measure aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "measure-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "message-stack.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/measure-tool.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::Util::Unit;
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Tools::MeasureTool;
+
+/** Temporary hack: Returns the node tool in the active desktop.
+ * Will go away during tool refactoring. */
+static MeasureTool *get_measure_tool()
+{
+ MeasureTool *tool = nullptr;
+ if (SP_ACTIVE_DESKTOP ) {
+ Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context;
+ if (SP_IS_MEASURE_CONTEXT(ec)) {
+ tool = static_cast<MeasureTool*>(ec);
+ }
+ }
+ return tool;
+}
+
+
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+MeasureToolbar::MeasureToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+{
+ auto prefs = Inkscape::Preferences::get();
+ _tracker->setActiveUnitByAbbr(prefs->getString("/tools/measure/unit").c_str());
+
+ /* Font Size */
+ {
+ auto font_size_val = prefs->getDouble("/tools/measure/fontsize", 10.0);
+ _font_size_adj = Gtk::Adjustment::create(font_size_val, 1.0, 36.0, 1.0, 4.0);
+ auto font_size_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-fontsize", _("Font Size:"), _font_size_adj, 0, 2));
+ font_size_item->set_tooltip_text(_("The font size to be used in the measurement labels"));
+ font_size_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _font_size_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::fontsize_value_changed));
+ add(*font_size_item);
+ }
+
+ /* Precision */
+ {
+ auto precision_val = prefs->getDouble("/tools/measure/precision", 2);
+ _precision_adj = Gtk::Adjustment::create(precision_val, 0, 10, 1, 0);
+ auto precision_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-precision", _("Precision:"), _precision_adj, 0, 0));
+ precision_item->set_tooltip_text(_("Decimal precision of measure"));
+ precision_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _precision_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::precision_value_changed));
+ add(*precision_item);
+ }
+
+ /* Scale */
+ {
+ auto scale_val = prefs->getDouble("/tools/measure/scale", 100.0);
+ _scale_adj = Gtk::Adjustment::create(scale_val, 0.0, 90000.0, 1.0, 4.0);
+ auto scale_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-scale", _("Scale %:"), _scale_adj, 0, 3));
+ scale_item->set_tooltip_text(_("Scale the results"));
+ scale_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::scale_value_changed));
+ add(*scale_item);
+ }
+
+ /* units label */
+ {
+ auto unit_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Units:")));
+ unit_label->set_tooltip_text(_("The units to be used for the measurements"));
+ unit_label->set_use_markup(true);
+ add(*unit_label);
+ }
+
+ /* units menu */
+ {
+ auto ti = _tracker->create_tool_item(_("Units:"), _("The units to be used for the measurements") );
+ ti->signal_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::unit_changed));
+ add(*ti);
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* measure only selected */
+ {
+ _only_selected_item = add_toggle_button(_("Measure only selected"),
+ _("Measure only selected"));
+ _only_selected_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center"));
+ _only_selected_item->set_active(prefs->getBool("/tools/measure/only_selected", false));
+ _only_selected_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_only_selected));
+ }
+
+ /* ignore_1st_and_last */
+ {
+ _ignore_1st_and_last_item = add_toggle_button(_("Ignore first and last"),
+ _("Ignore first and last"));
+ _ignore_1st_and_last_item->set_icon_name(INKSCAPE_ICON("draw-geometry-line-segment"));
+ _ignore_1st_and_last_item->set_active(prefs->getBool("/tools/measure/ignore_1st_and_last", true));
+ _ignore_1st_and_last_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_ignore_1st_and_last));
+ }
+
+ /* measure in betweens */
+ {
+ _inbetween_item = add_toggle_button(_("Show measures between items"),
+ _("Show measures between items"));
+ _inbetween_item->set_icon_name(INKSCAPE_ICON("distribute-randomize"));
+ _inbetween_item->set_active(prefs->getBool("/tools/measure/show_in_between", true));
+ _inbetween_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_in_between));
+ }
+
+ /* only visible */
+ {
+ _show_hidden_item = add_toggle_button(_("Show hidden intersections"),
+ _("Show hidden intersections"));
+ _show_hidden_item->set_icon_name(INKSCAPE_ICON("object-hidden"));
+ _show_hidden_item->set_active(prefs->getBool("/tools/measure/show_hidden", true));
+ _show_hidden_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_hidden)) ;
+ }
+
+ /* measure only current layer */
+ {
+ _all_layers_item = add_toggle_button(_("Measure all layers"),
+ _("Measure all layers"));
+ _all_layers_item->set_icon_name(INKSCAPE_ICON("dialog-layers"));
+ _all_layers_item->set_active(prefs->getBool("/tools/measure/all_layers", true));
+ _all_layers_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_all_layers));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* toggle start end */
+ {
+ _reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse measure")));
+ _reverse_item->set_tooltip_text(_("Reverse measure"));
+ _reverse_item->set_icon_name(INKSCAPE_ICON("draw-geometry-mirror"));
+ _reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::reverse_knots));
+ add(*_reverse_item);
+ }
+
+ /* phantom measure */
+ {
+ _to_phantom_item = Gtk::manage(new Gtk::ToolButton(_("Phantom measure")));
+ _to_phantom_item->set_tooltip_text(_("Phantom measure"));
+ _to_phantom_item->set_icon_name(INKSCAPE_ICON("selection-make-bitmap-copy"));
+ _to_phantom_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_phantom));
+ add(*_to_phantom_item);
+ }
+
+ /* to guides */
+ {
+ _to_guides_item = Gtk::manage(new Gtk::ToolButton(_("To guides")));
+ _to_guides_item->set_tooltip_text(_("To guides"));
+ _to_guides_item->set_icon_name(INKSCAPE_ICON("guides"));
+ _to_guides_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_guides));
+ add(*_to_guides_item);
+ }
+
+ /* to item */
+ {
+ _to_item_item = Gtk::manage(new Gtk::ToolButton(_("Convert to item")));
+ _to_item_item->set_tooltip_text(_("Convert to item"));
+ _to_item_item->set_icon_name(INKSCAPE_ICON("path-reverse"));
+ _to_item_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_item));
+ add(*_to_item_item);
+ }
+
+ /* to mark dimensions */
+ {
+ _mark_dimension_item = Gtk::manage(new Gtk::ToolButton(_("Mark Dimension")));
+ _mark_dimension_item->set_tooltip_text(_("Mark Dimension"));
+ _mark_dimension_item->set_icon_name(INKSCAPE_ICON("tool-pointer"));
+ _mark_dimension_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_mark_dimension));
+ add(*_mark_dimension_item);
+ }
+
+ /* Offset */
+ {
+ auto offset_val = prefs->getDouble("/tools/measure/offset", 5.0);
+ _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 90000.0, 1.0, 4.0);
+ auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-offset", _("Offset:"), _offset_adj, 0, 2));
+ offset_item->set_tooltip_text(_("Mark dimension offset"));
+ offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::offset_value_changed));
+ add(*offset_item);
+ }
+
+ show_all();
+}
+
+GtkWidget *
+MeasureToolbar::create(SPDesktop * desktop)
+{
+ auto toolbar = new MeasureToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+} // MeasureToolbar::prep()
+
+void
+MeasureToolbar::fontsize_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/measure/fontsize"),
+ _font_size_adj->get_value());
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+ }
+}
+
+void
+MeasureToolbar::unit_changed(int /* notUsed */)
+{
+ Glib::ustring const unit = _tracker->getActiveUnit()->abbr;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/tools/measure/unit", unit);
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::precision_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt(Glib::ustring("/tools/measure/precision"),
+ _precision_adj->get_value());
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+ }
+}
+
+void
+MeasureToolbar::scale_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/measure/scale"),
+ _scale_adj->get_value());
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+ }
+}
+
+void
+MeasureToolbar::offset_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/measure/offset"),
+ _offset_adj->get_value());
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+ }
+}
+
+void
+MeasureToolbar::toggle_only_selected()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _only_selected_item->get_active();
+ prefs->setBool("/tools/measure/only_selected", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measures only selected."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measure all."));
+ }
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::toggle_ignore_1st_and_last()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _ignore_1st_and_last_item->get_active();
+ prefs->setBool("/tools/measure/ignore_1st_and_last", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures inactive."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures active."));
+ }
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::toggle_show_in_between()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _inbetween_item->get_active();
+ prefs->setBool("/tools/measure/show_in_between", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute all elements."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute max length."));
+ }
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::toggle_show_hidden()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _show_hidden_item->get_active();
+ prefs->setBool("/tools/measure/show_hidden", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show all crossings."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show visible crossings."));
+ }
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::toggle_all_layers()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _all_layers_item->get_active();
+ prefs->setBool("/tools/measure/all_layers", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use all layers in the measure."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use current layer in the measure."));
+ }
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::reverse_knots()
+{
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->reverseKnots();
+ }
+}
+
+void
+MeasureToolbar::to_phantom()
+{
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->toPhantom();
+ }
+}
+
+void
+MeasureToolbar::to_guides()
+{
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->toGuides();
+ }
+}
+
+void
+MeasureToolbar::to_item()
+{
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->toItem();
+ }
+}
+
+void
+MeasureToolbar::to_mark_dimension()
+{
+ MeasureTool *mt = get_measure_tool();
+ if (mt) {
+ mt->toMarkDimension();
+ }
+}
+
+}
+}
+}
+
+
+/*
+ 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/toolbar/measure-toolbar.h b/src/ui/toolbar/measure-toolbar.h
new file mode 100644
index 0000000..a922fa1
--- /dev/null
+++ b/src/ui/toolbar/measure-toolbar.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_MEASURE_TOOLBAR_H
+#define SEEN_MEASURE_TOOLBAR_H
+
+/**
+ * @file
+ * Measure aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class UnitTracker;
+}
+
+namespace Toolbar {
+class MeasureToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+ Glib::RefPtr<Gtk::Adjustment> _font_size_adj;
+ Glib::RefPtr<Gtk::Adjustment> _precision_adj;
+ Glib::RefPtr<Gtk::Adjustment> _scale_adj;
+ Glib::RefPtr<Gtk::Adjustment> _offset_adj;
+
+ Gtk::ToggleToolButton *_only_selected_item;
+ Gtk::ToggleToolButton *_ignore_1st_and_last_item;
+ Gtk::ToggleToolButton *_inbetween_item;
+ Gtk::ToggleToolButton *_show_hidden_item;
+ Gtk::ToggleToolButton *_all_layers_item;
+
+ Gtk::ToolButton *_reverse_item;
+ Gtk::ToolButton *_to_phantom_item;
+ Gtk::ToolButton *_to_guides_item;
+ Gtk::ToolButton *_to_item_item;
+ Gtk::ToolButton *_mark_dimension_item;
+
+ void fontsize_value_changed();
+ void unit_changed(int notUsed);
+ void precision_value_changed();
+ void scale_value_changed();
+ void offset_value_changed();
+ void toggle_only_selected();
+ void toggle_ignore_1st_and_last();
+ void toggle_show_hidden();
+ void toggle_show_in_between();
+ void toggle_all_layers();
+ void reverse_knots();
+ void to_phantom();
+ void to_guides();
+ void to_item();
+ void to_mark_dimension();
+
+protected:
+ MeasureToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_MEASURE_TOOLBAR_H */
diff --git a/src/ui/toolbar/mesh-toolbar.cpp b/src/ui/toolbar/mesh-toolbar.cpp
new file mode 100644
index 0000000..c1ff08a
--- /dev/null
+++ b/src/ui/toolbar/mesh-toolbar.cpp
@@ -0,0 +1,621 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient aux toolbar
+ *
+ * Authors:
+ * bulia byak <bulia@dr.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Abhishek Sharma
+ * Tavmjong Bah <tavjong@free.fr>
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "mesh-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "gradient-drag.h"
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-stop.h"
+#include "style.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+#include "ui/tools/gradient-tool.h"
+#include "ui/tools/mesh-tool.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "widgets/gradient-image.h"
+#include "widgets/spinbutton-events.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Tools::MeshTool;
+
+static bool blocked = false;
+
+// Get a list of selected meshes taking into account fill/stroke toggles
+std::vector<SPMeshGradient *> ms_get_dt_selected_gradients(Inkscape::Selection *selection)
+{
+ std::vector<SPMeshGradient *> ms_selected;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool edit_fill = prefs->getBool("/tools/mesh/edit_fill", true);
+ bool edit_stroke = prefs->getBool("/tools/mesh/edit_stroke", true);
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;// get the items gradient, not the getVector() version
+ SPStyle *style = item->style;
+
+ if (style) {
+
+
+ if (edit_fill && style->fill.isPaintserver()) {
+ SPPaintServer *server = item->style->getFillPaintServer();
+ SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server);
+ if (mesh) {
+ ms_selected.push_back(mesh);
+ }
+ }
+
+ if (edit_stroke && style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server);
+ if (mesh) {
+ ms_selected.push_back(mesh);
+ }
+ }
+ }
+
+ }
+ return ms_selected;
+}
+
+
+/*
+ * Get the current selection status from the desktop
+ */
+void ms_read_selection( Inkscape::Selection *selection,
+ SPMeshGradient *&ms_selected,
+ bool &ms_selected_multi,
+ SPMeshType &ms_type,
+ bool &ms_type_multi )
+{
+ ms_selected = nullptr;
+ ms_selected_multi = false;
+ ms_type = SP_MESH_TYPE_COONS;
+ ms_type_multi = false;
+
+ bool first = true;
+
+ // Read desktop selection, taking into account fill/stroke toggles
+ std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients( selection );
+ for (auto & meshe : meshes) {
+ if (first) {
+ ms_selected = meshe;
+ ms_type = meshe->type;
+ first = false;
+ } else {
+ if (ms_selected != meshe) {
+ ms_selected_multi = true;
+ }
+ if (ms_type != meshe->type) {
+ ms_type_multi = true;
+ }
+ }
+ }
+}
+
+
+/*
+ * Callback functions for user actions
+ */
+
+
+/** Temporary hack: Returns the mesh tool in the active desktop.
+ * Will go away during tool refactoring. */
+static MeshTool *get_mesh_tool()
+{
+ MeshTool *tool = nullptr;
+ if (SP_ACTIVE_DESKTOP ) {
+ Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context;
+ if (SP_IS_MESH_CONTEXT(ec)) {
+ tool = static_cast<MeshTool*>(ec);
+ }
+ }
+ return tool;
+}
+
+
+static void mesh_toolbox_watch_ec(SPDesktop* dt, Inkscape::UI::Tools::ToolBase* ec, GObject* holder);
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+MeshToolbar::MeshToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _edit_fill_pusher(nullptr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* New mesh: normal or conical */
+ {
+ add_label(_("New:"));
+
+ Gtk::RadioToolButton::Group new_type_group;
+
+ auto normal_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("normal")));
+ normal_type_btn->set_tooltip_text(_("Create mesh gradient"));
+ normal_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-mesh"));
+ _new_type_buttons.push_back(normal_type_btn);
+
+ auto conical_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("conical")));
+ conical_type_btn->set_tooltip_text(_("Create conical gradient"));
+ conical_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-conical"));
+ _new_type_buttons.push_back(conical_type_btn);
+
+ int btn_idx = 0;
+ for (auto btn : _new_type_buttons) {
+ add(*btn);
+ btn->set_sensitive();
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_geometry_changed), btn_idx++));
+ }
+
+ gint mode = prefs->getInt("/tools/mesh/mesh_geometry", SP_MESH_GEOMETRY_NORMAL);
+ _new_type_buttons[mode]->set_active();
+ }
+
+ /* New gradient on fill or stroke*/
+ {
+ Gtk::RadioToolButton::Group new_fillstroke_group;
+
+ auto fill_button = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill")));
+ fill_button->set_tooltip_text(_("Create gradient in the fill"));
+ fill_button->set_icon_name(INKSCAPE_ICON("object-fill"));
+ _new_fillstroke_buttons.push_back(fill_button);
+
+ auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke")));
+ stroke_btn->set_tooltip_text(_("Create gradient in the stroke"));
+ stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke"));
+ _new_fillstroke_buttons.push_back(stroke_btn);
+
+ int btn_idx = 0;
+ for(auto btn : _new_fillstroke_buttons) {
+ add(*btn);
+ btn->set_sensitive(true);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_fillstroke_changed), btn_idx++));
+ }
+
+ gint mode = prefs->getInt("/tools/mesh/newfillorstroke");
+ _new_fillstroke_buttons[mode]->set_active();
+ }
+
+ /* Number of mesh rows */
+ {
+ std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ auto rows_val = prefs->getDouble("/tools/mesh/mesh_rows", 1);
+ _row_adj = Gtk::Adjustment::create(rows_val, 1, 20, 1, 1);
+ auto row_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-row", _("Rows:"), _row_adj, 1.0, 0));
+ row_item->set_tooltip_text(_("Number of rows in new mesh"));
+ row_item->set_custom_numeric_menu_data(values);
+ row_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _row_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::row_changed));
+ add(*row_item);
+ row_item->set_sensitive(true);
+ }
+
+ /* Number of mesh columns */
+ {
+ std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ auto col_val = prefs->getDouble("/tools/mesh/mesh_cols", 1);
+ _col_adj = Gtk::Adjustment::create(col_val, 1, 20, 1, 1);
+ auto col_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-col", _("Columns:"), _col_adj, 1.0, 0));
+ col_item->set_tooltip_text(_("Number of columns in new mesh"));
+ col_item->set_custom_numeric_menu_data(values);
+ col_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _col_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::col_changed));
+ add(*col_item);
+ col_item->set_sensitive(true);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ // TODO: These were disabled in the UI file. Either activate or delete
+#if 0
+ /* Edit fill mesh */
+ {
+ _edit_fill_item = add_toggle_button(_("Edit Fill"),
+ _("Edit fill mesh"));
+ _edit_fill_item->set_icon_name(INKSCAPE_ICON("object-fill"));
+ _edit_fill_pusher.reset(new UI::SimplePrefPusher(_edit_fill_item, "/tools/mesh/edit_fill"));
+ _edit_fill_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke));
+ }
+
+ /* Edit stroke mesh */
+ {
+ _edit_stroke_item = add_toggle_button(_("Edit Stroke"),
+ _("Edit stroke mesh"));
+ _edit_stroke_item->set_icon_name(INKSCAPE_ICON("object-stroke"));
+ _edit_stroke_pusher.reset(new UI::SimplePrefPusher(_edit_stroke_item, "/tools/mesh/edit_stroke"));
+ _edit_stroke_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke));
+ }
+
+ /* Show/hide side and tensor handles */
+ {
+ auto show_handles_item = add_toggle_button(_("Show Handles"),
+ _("Show handles"));
+ show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles"));
+ _show_handles_pusher.reset(new UI::SimplePrefPusher(show_handles_item, "/tools/mesh/show_handles"));
+ show_handles_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_handles));
+ }
+#endif
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &MeshToolbar::watch_ec));
+
+ {
+ auto btn = Gtk::manage(new Gtk::ToolButton(_("Toggle Sides")));
+ btn->set_tooltip_text(_("Toggle selected sides between Beziers and lines."));
+ btn->set_icon_name(INKSCAPE_ICON("node-segment-line"));
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_sides));
+ add(*btn);
+ }
+
+ {
+ auto btn = Gtk::manage(new Gtk::ToolButton(_("Make elliptical")));
+ btn->set_tooltip_text(_("Make selected sides elliptical by changing length of handles. Works best if handles already approximate ellipse."));
+ btn->set_icon_name(INKSCAPE_ICON("node-segment-curve"));
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::make_elliptical));
+ add(*btn);
+ }
+
+ {
+ auto btn = Gtk::manage(new Gtk::ToolButton(_("Pick colors:")));
+ btn->set_tooltip_text(_("Pick colors for selected corner nodes from underneath mesh."));
+ btn->set_icon_name(INKSCAPE_ICON("color-picker"));
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::pick_colors));
+ add(*btn);
+ }
+
+
+ {
+ auto btn = Gtk::manage(new Gtk::ToolButton(_("Scale mesh to bounding box:")));
+ btn->set_tooltip_text(_("Scale mesh to fit inside bounding box."));
+ btn->set_icon_name(INKSCAPE_ICON("mesh-gradient-fit"));
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::fit_mesh));
+ add(*btn);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Warning */
+ {
+ auto btn = Gtk::manage(new Gtk::ToolButton(_("WARNING: Mesh SVG Syntax Subject to Change")));
+ btn->set_tooltip_text(_("WARNING: Mesh SVG Syntax Subject to Change"));
+ btn->set_icon_name(INKSCAPE_ICON("dialog-warning"));
+ add(*btn);
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::warning_popup));
+ btn->set_sensitive(true);
+ }
+
+ /* Type */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = C_("Type", "Coons");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Bicubic");
+ row[columns.col_sensitive] = true;
+
+ _select_type_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Smoothing:"),
+ // TRANSLATORS: Type of Smoothing. See https://en.wikipedia.org/wiki/Coons_patch
+ _("Coons: no smoothing. Bicubic: smoothing across patch boundaries."),
+ "Not Used", store));
+ _select_type_item->use_group_label(true);
+
+ _select_type_item->set_active(0);
+
+ _select_type_item->signal_changed().connect(sigc::mem_fun(*this, &MeshToolbar::type_changed));
+ add(*_select_type_item);
+ }
+
+ show_all();
+}
+
+/**
+ * Mesh auxiliary toolbar construction and setup.
+ * Don't forget to add to XML in widgets/toolbox.cpp!
+ *
+ */
+GtkWidget *
+MeshToolbar::create(SPDesktop * desktop)
+{
+ auto toolbar = new MeshToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+MeshToolbar::new_geometry_changed(int mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/mesh/mesh_geometry", mode);
+}
+
+void
+MeshToolbar::new_fillstroke_changed(int mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/mesh/newfillorstroke", mode);
+}
+
+void
+MeshToolbar::row_changed()
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ int rows = _row_adj->get_value();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ prefs->setInt("/tools/mesh/mesh_rows", rows);
+
+ blocked = FALSE;
+}
+
+void
+MeshToolbar::col_changed()
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ int cols = _col_adj->get_value();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ prefs->setInt("/tools/mesh/mesh_cols", cols);
+
+ blocked = FALSE;
+}
+
+void
+MeshToolbar::toggle_fill_stroke()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("tools/mesh/edit_fill", _edit_fill_item->get_active());
+ prefs->setBool("tools/mesh/edit_stroke", _edit_stroke_item->get_active());
+
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ GrDrag *drag = mt->_grdrag;
+ drag->updateDraggers();
+ drag->updateLines();
+ drag->updateLevels();
+ selection_changed(nullptr); // Need to update Type widget
+ }
+}
+
+void
+MeshToolbar::toggle_handles()
+{
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ GrDrag *drag = mt->_grdrag;
+ drag->refreshDraggers();
+ }
+}
+
+void
+MeshToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (SP_IS_MESH_CONTEXT(ec)) {
+ // connect to selection modified and changed signals
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+
+ c_selection_changed = selection->connectChanged(sigc::mem_fun(*this, &MeshToolbar::selection_changed));
+ c_selection_modified = selection->connectModified(sigc::mem_fun(*this, &MeshToolbar::selection_modified));
+ c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &MeshToolbar::drag_selection_changed));
+
+ c_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &MeshToolbar::defs_release));
+ c_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &MeshToolbar::defs_modified));
+ selection_changed(selection);
+ } else {
+ if (c_selection_changed)
+ c_selection_changed.disconnect();
+ if (c_selection_modified)
+ c_selection_modified.disconnect();
+ if (c_subselection_changed)
+ c_subselection_changed.disconnect();
+ if (c_defs_release)
+ c_defs_release.disconnect();
+ if (c_defs_modified)
+ c_defs_modified.disconnect();
+ }
+}
+
+void
+MeshToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ selection_changed(selection);
+}
+
+void
+MeshToolbar::drag_selection_changed(gpointer /*dragger*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+MeshToolbar::defs_release(SPObject * /*defs*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+MeshToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/)
+{
+ selection_changed(nullptr);
+}
+
+/*
+ * Core function, setup all the widgets whenever something changes on the desktop
+ */
+void
+MeshToolbar::selection_changed(Inkscape::Selection * /* selection */)
+{
+ // std::cout << "ms_tb_selection_changed" << std::endl;
+
+ if (blocked)
+ return;
+
+ if (!_desktop) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args
+ if (selection) {
+ // ToolBase *ev = sp_desktop_event_context(desktop);
+ // GrDrag *drag = NULL;
+ // if (ev) {
+ // drag = ev->get_drag();
+ // // Hide/show handles?
+ // }
+
+ SPMeshGradient *ms_selected = nullptr;
+ SPMeshType ms_type = SP_MESH_TYPE_COONS;
+ bool ms_selected_multi = false;
+ bool ms_type_multi = false;
+ ms_read_selection( selection, ms_selected, ms_selected_multi, ms_type, ms_type_multi );
+ // std::cout << " type: " << ms_type << std::endl;
+
+ if (_select_type_item) {
+ _select_type_item->set_sensitive(!ms_type_multi);
+ blocked = TRUE;
+ _select_type_item->set_active(ms_type);
+ blocked = FALSE;
+ }
+ }
+}
+
+void
+MeshToolbar::warning_popup()
+{
+ char *msg = _("Mesh gradients are part of SVG 2:\n"
+ "* Syntax may change.\n"
+ "* Web browser implementation is not guaranteed.\n"
+ "\n"
+ "For web: convert to bitmap (Edit->Make bitmap copy).\n"
+ "For print: export to PDF.");
+ Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_WARNING,
+ Gtk::BUTTONS_OK, true);
+ dialog.run();
+}
+
+/**
+ * Sets mesh type: Coons, Bicubic
+ */
+void
+MeshToolbar::type_changed(int mode)
+{
+ if (blocked) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients(selection);
+
+ SPMeshType type = (SPMeshType) mode;
+ for (auto & meshe : meshes) {
+ meshe->type = type;
+ meshe->type_set = true;
+ meshe->updateRepr();
+ }
+ if (!meshes.empty() ) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_MESH,_("Set mesh type"));
+ }
+}
+
+void
+MeshToolbar::toggle_sides()
+{
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ sp_mesh_context_corner_operation( mt, MG_CORNER_SIDE_TOGGLE );
+ }
+}
+
+void
+MeshToolbar::make_elliptical()
+{
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ sp_mesh_context_corner_operation( mt, MG_CORNER_SIDE_ARC );
+ }
+}
+
+void
+MeshToolbar::pick_colors()
+{
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ sp_mesh_context_corner_operation( mt, MG_CORNER_COLOR_PICK );
+ }
+}
+
+void
+MeshToolbar::fit_mesh()
+{
+ MeshTool *mt = get_mesh_tool();
+ if (mt) {
+ sp_mesh_context_fit_mesh_in_bbox( mt );
+ }
+}
+
+
+}
+}
+}
+
+/*
+ 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/toolbar/mesh-toolbar.h b/src/ui/toolbar/mesh-toolbar.h
new file mode 100644
index 0000000..2df4411
--- /dev/null
+++ b/src/ui/toolbar/mesh-toolbar.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_MESH_TOOLBAR_H
+#define SEEN_MESH_TOOLBAR_H
+
+/*
+ * Mesh aux toolbar
+ *
+ * Authors:
+ * bulia byak <bulia@dr.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2012 authors
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPObject;
+
+namespace Gtk {
+class RadioToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+class SimplePrefPusher;
+
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class ComboToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class MeshToolbar : public Toolbar {
+private:
+ std::vector<Gtk::RadioToolButton *> _new_type_buttons;
+ std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons;
+ UI::Widget::ComboToolItem *_select_type_item;
+
+ Gtk::ToggleToolButton *_edit_fill_item;
+ Gtk::ToggleToolButton *_edit_stroke_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _row_adj;
+ Glib::RefPtr<Gtk::Adjustment> _col_adj;
+
+ std::unique_ptr<UI::SimplePrefPusher> _edit_fill_pusher;
+ std::unique_ptr<UI::SimplePrefPusher> _edit_stroke_pusher;
+ std::unique_ptr<UI::SimplePrefPusher> _show_handles_pusher;
+
+ sigc::connection c_selection_changed;
+ sigc::connection c_selection_modified;
+ sigc::connection c_subselection_changed;
+ sigc::connection c_defs_release;
+ sigc::connection c_defs_modified;
+
+ void new_geometry_changed(int mode);
+ void new_fillstroke_changed(int mode);
+ void row_changed();
+ void col_changed();
+ void toggle_fill_stroke();
+ void selection_changed(Inkscape::Selection *selection);
+ void toggle_handles();
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_modified(Inkscape::Selection *selection, guint flags);
+ void drag_selection_changed(gpointer dragger);
+ void defs_release(SPObject *defs);
+ void defs_modified(SPObject *defs, guint flags);
+ void warning_popup();
+ void type_changed(int mode);
+ void toggle_sides();
+ void make_elliptical();
+ void pick_colors();
+ void fit_mesh();
+
+protected:
+ MeshToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_MESH_TOOLBAR_H */
diff --git a/src/ui/toolbar/node-toolbar.cpp b/src/ui/toolbar/node-toolbar.cpp
new file mode 100644
index 0000000..9aa8328
--- /dev/null
+++ b/src/ui/toolbar/node-toolbar.cpp
@@ -0,0 +1,651 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Node aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "node-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/image.h>
+#include <gtkmm/menutoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "selection-chemistry.h"
+#include "verbs.h"
+
+#include "helper/action.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+#include "widgets/widget-sizes.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+using Inkscape::UI::Tools::NodeTool;
+
+/** Temporary hack: Returns the node tool in the active desktop.
+ * Will go away during tool refactoring. */
+static NodeTool *get_node_tool()
+{
+ 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<NodeTool*>(ec);
+ }
+ }
+ return tool;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+NodeToolbar::NodeToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _freeze(false)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ Unit doc_units = *desktop->getNamedView()->display_units;
+ _tracker->setActiveUnit(&doc_units);
+
+ {
+ auto insert_node_item = Gtk::manage(new Gtk::MenuToolButton());
+ insert_node_item->set_icon_name(INKSCAPE_ICON("node-add"));
+ insert_node_item->set_label(_("Insert node"));
+ insert_node_item->set_tooltip_text(_("Insert new nodes into selected segments"));
+ insert_node_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add));
+
+ auto insert_node_menu = Gtk::manage(new Gtk::Menu());
+
+ {
+ // TODO: Consider moving back to icons in menu?
+ //auto insert_min_x_icon = Gtk::manage(new Gtk::Image());
+ //insert_min_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_x"), Gtk::ICON_SIZE_MENU);
+ //auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(*insert_min_x_icon));
+ auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min X")));
+ insert_min_x_item->set_tooltip_text(_("Insert new nodes at min X into selected segments"));
+ insert_min_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_x));
+ insert_node_menu->append(*insert_min_x_item);
+ }
+ {
+ //auto insert_max_x_icon = Gtk::manage(new Gtk::Image());
+ //insert_max_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_x"), Gtk::ICON_SIZE_MENU);
+ //auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(*insert_max_x_icon));
+ auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max X")));
+ insert_max_x_item->set_tooltip_text(_("Insert new nodes at max X into selected segments"));
+ insert_max_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_x));
+ insert_node_menu->append(*insert_max_x_item);
+ }
+ {
+ //auto insert_min_y_icon = Gtk::manage(new Gtk::Image());
+ //insert_min_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_y"), Gtk::ICON_SIZE_MENU);
+ //auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(*insert_min_y_icon));
+ auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min Y")));
+ insert_min_y_item->set_tooltip_text(_("Insert new nodes at min Y into selected segments"));
+ insert_min_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_y));
+ insert_node_menu->append(*insert_min_y_item);
+ }
+ {
+ //auto insert_max_y_icon = Gtk::manage(new Gtk::Image());
+ //insert_max_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_y"), Gtk::ICON_SIZE_MENU);
+ //auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(*insert_max_y_icon));
+ auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max Y")));
+ insert_max_y_item->set_tooltip_text(_("Insert new nodes at max Y into selected segments"));
+ insert_max_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_y));
+ insert_node_menu->append(*insert_max_y_item);
+ }
+
+ insert_node_menu->show_all();
+ insert_node_item->set_menu(*insert_node_menu);
+ add(*insert_node_item);
+ }
+
+ {
+ auto delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete node")));
+ delete_item->set_tooltip_text(_("Delete selected nodes"));
+ delete_item->set_icon_name(INKSCAPE_ICON("node-delete"));
+ delete_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete));
+ add(*delete_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto join_item = Gtk::manage(new Gtk::ToolButton(_("Join nodes")));
+ join_item->set_tooltip_text(_("Join selected nodes"));
+ join_item->set_icon_name(INKSCAPE_ICON("node-join"));
+ join_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join));
+ add(*join_item);
+ }
+
+ {
+ auto break_item = Gtk::manage(new Gtk::ToolButton(_("Break nodes")));
+ break_item->set_tooltip_text(_("Break path at selected nodes"));
+ break_item->set_icon_name(INKSCAPE_ICON("node-break"));
+ break_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_break));
+ add(*break_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto join_segment_item = Gtk::manage(new Gtk::ToolButton(_("Join with segment")));
+ join_segment_item->set_tooltip_text(_("Join selected endnodes with a new segment"));
+ join_segment_item->set_icon_name(INKSCAPE_ICON("node-join-segment"));
+ join_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join_segment));
+ add(*join_segment_item);
+ }
+
+ {
+ auto delete_segment_item = Gtk::manage(new Gtk::ToolButton(_("Delete segment")));
+ delete_segment_item->set_tooltip_text(_("Delete segment between two non-endpoint nodes"));
+ delete_segment_item->set_icon_name(INKSCAPE_ICON("node-delete-segment"));
+ delete_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete_segment));
+ add(*delete_segment_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto cusp_item = Gtk::manage(new Gtk::ToolButton(_("Node Cusp")));
+ cusp_item->set_tooltip_text(_("Make selected nodes corner"));
+ cusp_item->set_icon_name(INKSCAPE_ICON("node-type-cusp"));
+ cusp_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_cusp));
+ add(*cusp_item);
+ }
+
+ {
+ auto smooth_item = Gtk::manage(new Gtk::ToolButton(_("Node Smooth")));
+ smooth_item->set_tooltip_text(_("Make selected nodes smooth"));
+ smooth_item->set_icon_name(INKSCAPE_ICON("node-type-smooth"));
+ smooth_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_smooth));
+ add(*smooth_item);
+ }
+
+ {
+ auto symmetric_item = Gtk::manage(new Gtk::ToolButton(_("Node Symmetric")));
+ symmetric_item->set_tooltip_text(_("Make selected nodes symmetric"));
+ symmetric_item->set_icon_name(INKSCAPE_ICON("node-type-symmetric"));
+ symmetric_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_symmetrical));
+ add(*symmetric_item);
+ }
+
+ {
+ auto auto_item = Gtk::manage(new Gtk::ToolButton(_("Node Auto")));
+ auto_item->set_tooltip_text(_("Make selected nodes auto-smooth"));
+ auto_item->set_icon_name(INKSCAPE_ICON("node-type-auto-smooth"));
+ auto_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_auto));
+ add(*auto_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto line_item = Gtk::manage(new Gtk::ToolButton(_("Node Line")));
+ line_item->set_tooltip_text(_("Make selected segments lines"));
+ line_item->set_icon_name(INKSCAPE_ICON("node-segment-line"));
+ line_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_toline));
+ add(*line_item);
+ }
+
+ {
+ auto curve_item = Gtk::manage(new Gtk::ToolButton(_("Node Curve")));
+ curve_item->set_tooltip_text(_("Make selected segments curves"));
+ curve_item->set_icon_name(INKSCAPE_ICON("node-segment-curve"));
+ curve_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_tocurve));
+ add(*curve_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ auto context = Inkscape::ActionContext(_desktop);
+
+ {
+ auto object_to_path_item = SPAction::create_toolbutton_for_verb(SP_VERB_OBJECT_TO_CURVE, context);
+ add(*object_to_path_item);
+ }
+
+ {
+ auto stroke_to_path_item = SPAction::create_toolbutton_for_verb(SP_VERB_SELECTION_OUTLINE, context);
+ add(*stroke_to_path_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* X coord of selected node(s) */
+ {
+ std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ auto nodes_x_val = prefs->getDouble("/tools/nodes/Xcoord", 0);
+ _nodes_x_adj = Gtk::Adjustment::create(nodes_x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _nodes_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-x", _("X:"), _nodes_x_adj));
+ _nodes_x_item->set_tooltip_text(_("X coordinate of selected node(s)"));
+ _nodes_x_item->set_custom_numeric_menu_data(values);
+ _tracker->addAdjustment(_nodes_x_adj->gobj());
+ _nodes_x_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _nodes_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::X));
+ _nodes_x_item->set_sensitive(false);
+ add(*_nodes_x_item);
+ }
+
+ /* Y coord of selected node(s) */
+ {
+ std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ auto nodes_y_val = prefs->getDouble("/tools/nodes/Ycoord", 0);
+ _nodes_y_adj = Gtk::Adjustment::create(nodes_y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _nodes_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-y", _("Y:"), _nodes_y_adj));
+ _nodes_y_item->set_tooltip_text(_("Y coordinate of selected node(s)"));
+ _nodes_y_item->set_custom_numeric_menu_data(values);
+ _tracker->addAdjustment(_nodes_y_adj->gobj());
+ _nodes_y_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _nodes_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::Y));
+ _nodes_y_item->set_sensitive(false);
+ add(*_nodes_y_item);
+ }
+
+ // add the units menu
+ {
+ auto unit_menu = _tracker->create_tool_item(_("Units"), (""));
+ add(*unit_menu);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ _object_edit_clip_path_item = add_toggle_button(_("Edit clipping paths"),
+ _("Show clipping path(s) of selected object(s)"));
+ _object_edit_clip_path_item->set_icon_name(INKSCAPE_ICON("path-clip-edit"));
+ _pusher_edit_clipping_paths.reset(new SimplePrefPusher(_object_edit_clip_path_item, "/tools/nodes/edit_clipping_paths"));
+ _object_edit_clip_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled),
+ _object_edit_clip_path_item,
+ "/tools/nodes/edit_clipping_paths"));
+ }
+
+ {
+ _object_edit_mask_path_item = add_toggle_button(_("Edit masks"),
+ _("Show mask(s) of selected object(s)"));
+ _object_edit_mask_path_item->set_icon_name(INKSCAPE_ICON("path-mask-edit"));
+ _pusher_edit_masks.reset(new SimplePrefPusher(_object_edit_mask_path_item, "/tools/nodes/edit_masks"));
+ _object_edit_mask_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled),
+ _object_edit_mask_path_item,
+ "/tools/nodes/edit_masks"));
+ }
+
+ {
+ _nodes_lpeedit_item = SPAction::create_toolbutton_for_verb(SP_VERB_EDIT_NEXT_PATHEFFECT_PARAMETER, context);
+ add(*_nodes_lpeedit_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ _show_transform_handles_item = add_toggle_button(_("Show Transform Handles"),
+ _("Show transformation handles for selected nodes"));
+ _show_transform_handles_item->set_icon_name(INKSCAPE_ICON("node-transform"));
+ _pusher_show_transform_handles.reset(new UI::SimplePrefPusher(_show_transform_handles_item, "/tools/nodes/show_transform_handles"));
+ _show_transform_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled),
+ _show_transform_handles_item,
+ "/tools/nodes/show_transform_handles"));
+ }
+
+ {
+ _show_handles_item = add_toggle_button(_("Show Handles"),
+ _("Show Bezier handles of selected nodes"));
+ _show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles"));
+ _pusher_show_handles.reset(new UI::SimplePrefPusher(_show_handles_item, "/tools/nodes/show_handles"));
+ _show_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled),
+ _show_handles_item,
+ "/tools/nodes/show_handles"));
+ }
+
+ {
+ _show_helper_path_item = add_toggle_button(_("Show Outline"),
+ _("Show path outline (without path effects)"));
+ _show_helper_path_item->set_icon_name(INKSCAPE_ICON("show-path-outline"));
+ _pusher_show_outline.reset(new UI::SimplePrefPusher(_show_helper_path_item, "/tools/nodes/show_outline"));
+ _show_helper_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled),
+ _show_helper_path_item,
+ "/tools/nodes/show_outline"));
+ }
+
+ sel_changed(desktop->getSelection());
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &NodeToolbar::watch_ec));
+
+ show_all();
+}
+
+GtkWidget *
+NodeToolbar::create(SPDesktop *desktop)
+{
+ auto holder = new NodeToolbar(desktop);
+ return GTK_WIDGET(holder->gobj());
+} // NodeToolbar::prep()
+
+void
+NodeToolbar::value_changed(Geom::Dim2 d)
+{
+ auto adj = (d == Geom::X) ? _nodes_x_adj : _nodes_y_adj;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (!_tracker) {
+ return;
+ }
+
+ Unit const *unit = _tracker->getActiveUnit();
+
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ prefs->setDouble(Glib::ustring("/tools/nodes/") + (d == Geom::X ? "x" : "y"),
+ Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze || _tracker->isUpdating()) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ NodeTool *nt = get_node_tool();
+ if (nt && !nt->_selected_nodes->empty()) {
+ double val = Quantity::convert(adj->get_value(), unit, "px");
+ double oldval = nt->_selected_nodes->pointwiseBounds()->midpoint()[d];
+ Geom::Point delta(0,0);
+ delta[d] = val - oldval;
+ nt->_multipath->move(delta);
+ }
+
+ _freeze = false;
+}
+
+void
+NodeToolbar::sel_changed(Inkscape::Selection *selection)
+{
+ SPItem *item = selection->singleItem();
+ if (item && SP_IS_LPE_ITEM(item)) {
+ if (SP_LPE_ITEM(item)->hasPathEffect()) {
+ _nodes_lpeedit_item->set_sensitive(true);
+ } else {
+ _nodes_lpeedit_item->set_sensitive(false);
+ }
+ } else {
+ _nodes_lpeedit_item->set_sensitive(false);
+ }
+}
+
+void
+NodeToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (INK_IS_NODE_TOOL(ec)) {
+ // watch selection
+ c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &NodeToolbar::sel_changed));
+ c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &NodeToolbar::sel_modified));
+ c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &NodeToolbar::coord_changed));
+
+ sel_changed(desktop->getSelection());
+ } else {
+ if (c_selection_changed)
+ c_selection_changed.disconnect();
+ if (c_selection_modified)
+ c_selection_modified.disconnect();
+ if (c_subselection_changed)
+ c_subselection_changed.disconnect();
+ }
+}
+
+void
+NodeToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ sel_changed(selection);
+}
+
+/* is called when the node selection is modified */
+void
+NodeToolbar::coord_changed(gpointer /*shape_editor*/)
+{
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ if (!_tracker) {
+ return;
+ }
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ NodeTool *nt = get_node_tool();
+ if (!nt || !(nt->_selected_nodes) ||nt->_selected_nodes->empty()) {
+ // no path selected
+ _nodes_x_item->set_sensitive(false);
+ _nodes_y_item->set_sensitive(false);
+ } else {
+ _nodes_x_item->set_sensitive(true);
+ _nodes_y_item->set_sensitive(true);
+ Geom::Coord oldx = Quantity::convert(_nodes_x_adj->get_value(), unit, "px");
+ Geom::Coord oldy = Quantity::convert(_nodes_y_adj->get_value(), unit, "px");
+ Geom::Point mid = nt->_selected_nodes->pointwiseBounds()->midpoint();
+
+ if (oldx != mid[Geom::X]) {
+ _nodes_x_adj->set_value(Quantity::convert(mid[Geom::X], "px", unit));
+ }
+ if (oldy != mid[Geom::Y]) {
+ _nodes_y_adj->set_value(Quantity::convert(mid[Geom::Y], "px", unit));
+ }
+ }
+
+ _freeze = false;
+}
+
+void
+NodeToolbar::edit_add()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->insertNodes();
+ }
+}
+
+void
+NodeToolbar::edit_add_min_x()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_X);
+ }
+}
+
+void
+NodeToolbar::edit_add_max_x()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_X);
+ }
+}
+
+void
+NodeToolbar::edit_add_min_y()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_Y);
+ }
+}
+
+void
+NodeToolbar::edit_add_max_y()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_Y);
+ }
+}
+
+void
+NodeToolbar::edit_delete()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ nt->_multipath->deleteNodes(prefs->getBool("/tools/nodes/delete_preserves_shape", true));
+ }
+}
+
+void
+NodeToolbar::edit_join()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->joinNodes();
+ }
+}
+
+void
+NodeToolbar::edit_break()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->breakNodes();
+ }
+}
+
+void
+NodeToolbar::edit_delete_segment()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->deleteSegments();
+ }
+}
+
+void
+NodeToolbar::edit_join_segment()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->joinSegments();
+ }
+}
+
+void
+NodeToolbar::edit_cusp()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setNodeType(Inkscape::UI::NODE_CUSP);
+ }
+}
+
+void
+NodeToolbar::edit_smooth()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setNodeType(Inkscape::UI::NODE_SMOOTH);
+ }
+}
+
+void
+NodeToolbar::edit_symmetrical()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setNodeType(Inkscape::UI::NODE_SYMMETRIC);
+ }
+}
+
+void
+NodeToolbar::edit_auto()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setNodeType(Inkscape::UI::NODE_AUTO);
+ }
+}
+
+void
+NodeToolbar::edit_toline()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT);
+ }
+}
+
+void
+NodeToolbar::edit_tocurve()
+{
+ NodeTool *nt = get_node_tool();
+ if (nt) {
+ nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_CUBIC_BEZIER);
+ }
+}
+
+void
+NodeToolbar::on_pref_toggled(Gtk::ToggleToolButton *item,
+ const Glib::ustring& path)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(path, item->get_active());
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/node-toolbar.h b/src/ui/toolbar/node-toolbar.h
new file mode 100644
index 0000000..fc603cb
--- /dev/null
+++ b/src/ui/toolbar/node-toolbar.h
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_NODE_TOOLBAR_H
+#define SEEN_NODE_TOOLBAR_H
+
+/**
+ * @file
+ * Node aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+#include "2geom/coord.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+class SimplePrefPusher;
+
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class NodeToolbar : public Toolbar {
+private:
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+
+ std::unique_ptr<UI::SimplePrefPusher> _pusher_show_transform_handles;
+ std::unique_ptr<UI::SimplePrefPusher> _pusher_show_handles;
+ std::unique_ptr<UI::SimplePrefPusher> _pusher_show_outline;
+ std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_clipping_paths;
+ std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_masks;
+
+ Gtk::ToggleToolButton *_object_edit_clip_path_item;
+ Gtk::ToggleToolButton *_object_edit_mask_path_item;
+ Gtk::ToggleToolButton *_show_transform_handles_item;
+ Gtk::ToggleToolButton *_show_handles_item;
+ Gtk::ToggleToolButton *_show_helper_path_item;
+
+ Gtk::ToolButton *_nodes_lpeedit_item;
+
+ UI::Widget::SpinButtonToolItem *_nodes_x_item;
+ UI::Widget::SpinButtonToolItem *_nodes_y_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _nodes_x_adj;
+ Glib::RefPtr<Gtk::Adjustment> _nodes_y_adj;
+
+ bool _freeze;
+
+ sigc::connection c_selection_changed;
+ sigc::connection c_selection_modified;
+ sigc::connection c_subselection_changed;
+
+ void value_changed(Geom::Dim2 d);
+ void sel_changed(Inkscape::Selection *selection);
+ void sel_modified(Inkscape::Selection *selection, guint /*flags*/);
+ void coord_changed(gpointer shape_editor);
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void edit_add();
+ void edit_add_min_x();
+ void edit_add_max_x();
+ void edit_add_min_y();
+ void edit_add_max_y();
+ void edit_delete();
+ void edit_join();
+ void edit_break();
+ void edit_join_segment();
+ void edit_delete_segment();
+ void edit_cusp();
+ void edit_smooth();
+ void edit_symmetrical();
+ void edit_auto();
+ void edit_toline();
+ void edit_tocurve();
+ void on_pref_toggled(Gtk::ToggleToolButton *item,
+ const Glib::ustring& path);
+
+protected:
+ NodeToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+}
+}
+}
+#endif /* !SEEN_SELECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/paintbucket-toolbar.cpp b/src/ui/toolbar/paintbucket-toolbar.cpp
new file mode 100644
index 0000000..fc0b394
--- /dev/null
+++ b/src/ui/toolbar/paintbucket-toolbar.cpp
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Paint bucket aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "paintbucket-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/flood-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+PaintbucketToolbar::PaintbucketToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ // Channel
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ for (auto item: Inkscape::UI::Tools::FloodTool::channel_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _channels_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Fill by:"), Glib::ustring(), "Not Used", store));
+ _channels_item->use_group_label(true);
+
+ int channels = prefs->getInt("/tools/paintbucket/channels", 0);
+ _channels_item->set_active(channels);
+
+ _channels_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::channels_changed));
+ add(*_channels_item);
+ }
+
+ // Spacing spinbox
+ {
+ auto threshold_val = prefs->getDouble("/tools/paintbucket/threshold", 5);
+ _threshold_adj = Gtk::Adjustment::create(threshold_val, 0, 100.0, 1.0, 10.0);
+ auto threshold_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-threshold", _("Threshold:"), _threshold_adj, 1, 0));
+ threshold_item->set_tooltip_text(_("The maximum allowed difference between the clicked pixel and the neighboring pixels to be counted in the fill"));
+ threshold_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _threshold_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::threshold_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*threshold_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ // Create the units menu.
+ Glib::ustring stored_unit = prefs->getString("/tools/paintbucket/offsetunits");
+ if (!stored_unit.empty()) {
+ Unit const *u = unit_table.getUnit(stored_unit);
+ _tracker->setActiveUnit(u);
+ }
+
+ // Offset spinbox
+ {
+ auto offset_val = prefs->getDouble("/tools/paintbucket/offset", 0);
+ _offset_adj = Gtk::Adjustment::create(offset_val, -1e4, 1e4, 0.1, 0.5);
+ auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-offset", _("Grow/shrink by:"), _offset_adj, 1, 2));
+ offset_item->set_tooltip_text(_("The amount to grow (positive) or shrink (negative) the created fill path"));
+ _tracker->addAdjustment(_offset_adj->gobj());
+ offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::offset_changed));
+ add(*offset_item);
+ }
+
+ {
+ auto unit_menu = _tracker->create_tool_item(_("Units"), (""));
+ add(*unit_menu);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Auto Gap */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ for (auto item: Inkscape::UI::Tools::FloodTool::gap_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _autogap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Close gaps:"), Glib::ustring(), "Not Used", store));
+ _autogap_item->use_group_label(true);
+
+ int autogap = prefs->getInt("/tools/paintbucket/autogap", 0);
+ _autogap_item->set_active(autogap);
+
+ _autogap_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::autogap_changed));
+ add(*_autogap_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Reset */
+ {
+ auto reset_button = Gtk::manage(new Gtk::ToolButton(_("Defaults")));
+ reset_button->set_tooltip_text(_("Reset paint bucket parameters to defaults (use Inkscape Preferences > Tools to change defaults)"));
+ reset_button->set_icon_name(INKSCAPE_ICON("edit-clear"));
+ reset_button->signal_clicked().connect(sigc::mem_fun(*this, &PaintbucketToolbar::defaults));
+ add(*reset_button);
+ reset_button->set_sensitive(true);
+ }
+
+ show_all();
+}
+
+GtkWidget *
+PaintbucketToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new PaintbucketToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+PaintbucketToolbar::channels_changed(int channels)
+{
+ Inkscape::UI::Tools::FloodTool::set_channels(channels);
+}
+
+void
+PaintbucketToolbar::threshold_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/paintbucket/threshold", (gint)_threshold_adj->get_value());
+}
+
+void
+PaintbucketToolbar::offset_changed()
+{
+ Unit const *unit = _tracker->getActiveUnit();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Don't adjust the offset value because we're saving the
+ // unit and it'll be correctly handled on load.
+ prefs->setDouble("/tools/paintbucket/offset", (gdouble)_offset_adj->get_value());
+
+ g_return_if_fail(unit != nullptr);
+ prefs->setString("/tools/paintbucket/offsetunits", unit->abbr);
+}
+
+void
+PaintbucketToolbar::autogap_changed(int autogap)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/paintbucket/autogap", autogap);
+}
+
+void
+PaintbucketToolbar::defaults()
+{
+ // FIXME: make defaults settable via Inkscape Options
+ _threshold_adj->set_value(15);
+ _offset_adj->set_value(0.0);
+
+ _channels_item->set_active(Inkscape::UI::Tools::FLOOD_CHANNELS_RGB);
+ _autogap_item->set_active(0);
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/paintbucket-toolbar.h b/src/ui/toolbar/paintbucket-toolbar.h
new file mode 100644
index 0000000..d1b1a77
--- /dev/null
+++ b/src/ui/toolbar/paintbucket-toolbar.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PAINTBUCKET_TOOLBAR_H
+#define SEEN_PAINTBUCKET_TOOLBAR_H
+
+/**
+ * @file
+ * Paintbucket aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class UnitTracker;
+class ComboToolItem;
+}
+
+namespace Toolbar {
+class PaintbucketToolbar : public Toolbar {
+private:
+ UI::Widget::ComboToolItem *_channels_item;
+ UI::Widget::ComboToolItem *_autogap_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _threshold_adj;
+ Glib::RefPtr<Gtk::Adjustment> _offset_adj;
+
+ UI::Widget::UnitTracker *_tracker;
+
+ void channels_changed(int channels);
+ void threshold_changed();
+ void offset_changed();
+ void autogap_changed(int autogap);
+ void defaults();
+
+protected:
+ PaintbucketToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_PAINTBUCKET_TOOLBAR_H */
diff --git a/src/ui/toolbar/pencil-toolbar.cpp b/src/ui/toolbar/pencil-toolbar.cpp
new file mode 100644
index 0000000..2b7fdb2
--- /dev/null
+++ b/src/ui/toolbar/pencil-toolbar.cpp
@@ -0,0 +1,622 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Pencil aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include "pencil-toolbar.h"
+
+#include "desktop.h"
+#include "selection.h"
+
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/lpe-powerstroke.h"
+#include "live_effects/lpe-simplify.h"
+#include "live_effects/lpe-spiro.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+
+#include "display/curve.h"
+
+#include "object/sp-shape.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/pen-tool.h"
+
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "ui/uxmanager.h"
+
+#include "widgets/spinbutton-events.h"
+
+using Inkscape::UI::UXManager;
+
+/*
+class PencilToleranceObserver : public Inkscape::Preferences::Observer {
+public:
+ PencilToleranceObserver(Glib::ustring const &path, GObject *x) : Observer(path), _obj(x)
+ {
+ g_object_set_data(_obj, "prefobserver", this);
+ }
+ virtual ~PencilToleranceObserver() {
+ if (g_object_get_data(_obj, "prefobserver") == this) {
+ g_object_set_data(_obj, "prefobserver", NULL);
+ }
+ }
+ virtual void notify(Inkscape::Preferences::Entry const &val) {
+ GObject* tbl = _obj;
+ if (g_object_get_data( tbl, "freeze" )) {
+ return;
+ }
+ g_object_set_data( tbl, "freeze", GINT_TO_POINTER(TRUE) );
+
+ GtkAdjustment * adj = GTK_ADJUSTMENT(g_object_get_data(tbl, "tolerance"));
+
+ double v = val.getDouble(adj->value);
+ gtk_adjustment_set_value(adj, v);
+ g_object_set_data( tbl, "freeze", GINT_TO_POINTER(FALSE) );
+ }
+private:
+ GObject *_obj;
+};
+*/
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+PencilToolbar::PencilToolbar(SPDesktop *desktop,
+ bool pencil_mode)
+ : Toolbar(desktop),
+ _repr(nullptr),
+ _freeze(false),
+ _flatten_simplify(nullptr),
+ _simplify(nullptr)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ add_freehand_mode_toggle(pencil_mode);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ if (pencil_mode) {
+ /* Use pressure */
+ {
+ _pressure_item = add_toggle_button(_("Use pressure input"), _("Use pressure input"));
+ _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ bool pressure = prefs->getBool(freehand_tool_name() + "/pressure", false);
+ _pressure_item->set_active(pressure);
+ _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::use_pencil_pressure));
+ }
+ /* min pressure */
+ {
+ auto minpressure_val = prefs->getDouble("/tools/freehand/pencil/minpressure", 0);
+ _minpressure_adj = Gtk::Adjustment::create(minpressure_val, 0, 100, 1, 0);
+ _minpressure =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-minpressure", _("Min:"), _minpressure_adj, 0, 0));
+ _minpressure->set_tooltip_text(_("Min percent of pressure"));
+ _minpressure->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _minpressure_adj->signal_value_changed().connect(
+ sigc::mem_fun(*this, &PencilToolbar::minpressure_value_changed));
+ add(*_minpressure);
+ }
+ /* max pressure */
+ {
+ auto maxpressure_val = prefs->getDouble("/tools/freehand/pencil/maxpressure", 30);
+ _maxpressure_adj = Gtk::Adjustment::create(maxpressure_val, 0, 100, 1, 0);
+ _maxpressure =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-maxpressure", _("Max:"), _maxpressure_adj, 0, 0));
+ _maxpressure->set_tooltip_text(_("Max percent of pressure"));
+ _maxpressure->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _maxpressure_adj->signal_value_changed().connect(
+ sigc::mem_fun(*this, &PencilToolbar::maxpressure_value_changed));
+ add(*_maxpressure);
+ }
+
+ /* powerstoke */
+ add_powerstroke_cap(pencil_mode);
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Tolerance */
+ {
+ std::vector<Glib::ustring> labels = { _("(many nodes, rough)"), _("(default)"), "", "", "", "",
+ _("(few nodes, smooth)") };
+ std::vector<double> values = { 1, 10, 20, 30, 50, 75, 100 };
+ auto tolerance_val = prefs->getDouble("/tools/freehand/pencil/tolerance", 3.0);
+ _tolerance_adj = Gtk::Adjustment::create(tolerance_val, 0, 100.0, 0.5, 1.0);
+ auto tolerance_item =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-tolerance", _("Smoothing:"), _tolerance_adj, 1, 2));
+ tolerance_item->set_tooltip_text(_("How much smoothing (simplifying) is applied to the line"));
+ tolerance_item->set_custom_numeric_menu_data(values, labels);
+ tolerance_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _tolerance_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::tolerance_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*tolerance_item);
+ }
+
+ /* LPE simplify based tolerance */
+ {
+ _simplify = add_toggle_button(_("LPE based interactive simplify"), _("LPE based interactive simplify"));
+ _simplify->set_icon_name(INKSCAPE_ICON("interactive_simplify"));
+ _simplify->set_active(prefs->getInt("/tools/freehand/pencil/simplify", 0));
+ _simplify->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_lpe));
+ }
+
+ /* LPE simplify flatten */
+ {
+ _flatten_simplify = Gtk::manage(new Gtk::ToolButton(_("LPE simplify flatten")));
+ _flatten_simplify->set_tooltip_text(_("LPE simplify flatten"));
+ _flatten_simplify->set_icon_name(INKSCAPE_ICON("flatten"));
+ _flatten_simplify->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_flatten));
+ add(*_flatten_simplify);
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+ }
+
+ /* advanced shape options */
+ add_advanced_shape_options(pencil_mode);
+
+ show_all();
+
+ // Elements must be hidden after show_all() is called
+ guint freehandMode = prefs->getInt(( pencil_mode ?
+ "/tools/freehand/pencil/freehand-mode" :
+ "/tools/freehand/pen/freehand-mode" ), 0);
+ if (freehandMode != 1 && freehandMode != 2) {
+ _flatten_spiro_bspline->set_visible(false);
+ }
+ if (pencil_mode) {
+ use_pencil_pressure();
+ }
+}
+
+GtkWidget *
+PencilToolbar::create_pencil(SPDesktop *desktop)
+{
+ auto toolbar = new PencilToolbar(desktop, true);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+PencilToolbar::~PencilToolbar()
+{
+ if(_repr) {
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+void
+PencilToolbar::mode_changed(int mode)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(freehand_tool_name() + "/freehand-mode", mode);
+
+ if (mode == 1 || mode == 2) {
+ _flatten_spiro_bspline->set_visible(true);
+ } else {
+ _flatten_spiro_bspline->set_visible(false);
+ }
+
+ bool visible = (mode != 2);
+
+ if (_simplify) {
+ _simplify->set_visible(visible);
+ if (_flatten_simplify) {
+ _flatten_simplify->set_visible(visible && _simplify->get_active());
+ }
+ }
+ if (tools_isactive(_desktop, TOOLS_FREEHAND_PEN)) {
+ SP_PEN_CONTEXT(_desktop->event_context)->setPolylineMode();
+ }
+}
+
+/* This is used in generic functions below to share large portions of code between pen and pencil tool */
+Glib::ustring const
+PencilToolbar::freehand_tool_name()
+{
+ return ( tools_isactive(_desktop, TOOLS_FREEHAND_PEN)
+ ? "/tools/freehand/pen"
+ : "/tools/freehand/pencil" );
+}
+
+void
+PencilToolbar::add_freehand_mode_toggle(bool tool_is_pencil)
+{
+ auto label = Gtk::manage(new UI::Widget::LabelToolItem(_("Mode:")));
+ label->set_tooltip_text(_("Mode of new lines drawn by this tool"));
+ add(*label);
+ /* Freehand mode toggle buttons */
+ Gtk::RadioToolButton::Group mode_group;
+ auto bezier_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Bezier")));
+ bezier_mode_btn->set_tooltip_text(_("Create regular Bezier path"));
+ bezier_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bezier"));
+ _mode_buttons.push_back(bezier_mode_btn);
+
+ auto spiro_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spiro")));
+ spiro_mode_btn->set_tooltip_text(_("Create Spiro path"));
+ spiro_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-spiro"));
+ _mode_buttons.push_back(spiro_mode_btn);
+
+ auto bspline_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("BSpline")));
+ bspline_mode_btn->set_tooltip_text(_("Create BSpline path"));
+ bspline_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bspline"));
+ _mode_buttons.push_back(bspline_mode_btn);
+
+ if (!tool_is_pencil) {
+ auto zigzag_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Zigzag")));
+ zigzag_mode_btn->set_tooltip_text(_("Create a sequence of straight line segments"));
+ zigzag_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline"));
+ _mode_buttons.push_back(zigzag_mode_btn);
+
+ auto paraxial_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Paraxial")));
+ paraxial_mode_btn->set_tooltip_text(_("Create a sequence of paraxial line segments"));
+ paraxial_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline-paraxial"));
+ _mode_buttons.push_back(paraxial_mode_btn);
+ }
+
+ int btn_idx = 0;
+ for (auto btn : _mode_buttons) {
+ btn->set_sensitive(true);
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &PencilToolbar::mode_changed), btn_idx++));
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* LPE bspline spiro flatten */
+ _flatten_spiro_bspline = Gtk::manage(new Gtk::ToolButton(_("LPE spiro or bspline flatten")));
+ _flatten_spiro_bspline->set_tooltip_text(_("LPE spiro or bspline flatten"));
+ _flatten_spiro_bspline->set_icon_name(INKSCAPE_ICON("flatten"));
+ _flatten_spiro_bspline->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::flatten_spiro_bspline));
+ add(*_flatten_spiro_bspline);
+
+ guint freehandMode = prefs->getInt(( tool_is_pencil ?
+ "/tools/freehand/pencil/freehand-mode" :
+ "/tools/freehand/pen/freehand-mode" ), 0);
+ // freehandMode range is (0,5] for the pen tool, (0,3] for the pencil tool
+ // freehandMode = 3 is an old way of signifying pressure, set it to 0.
+ _mode_buttons[(freehandMode < _mode_buttons.size()) ? freehandMode : 0]->set_active();
+}
+
+void
+PencilToolbar::minpressure_value_changed()
+{
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/freehand/pencil/minpressure", _minpressure_adj->get_value());
+}
+
+void
+PencilToolbar::maxpressure_value_changed()
+{
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/freehand/pencil/maxpressure", _maxpressure_adj->get_value());
+}
+
+void
+PencilToolbar::use_pencil_pressure() {
+ // assumes called by pencil toolbar (and all these widgets exist)
+ bool pressure = _pressure_item->get_active();
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(freehand_tool_name() + "/pressure", pressure);
+ if (pressure) {
+ _minpressure->set_visible(true);
+ _maxpressure->set_visible(true);
+ _cap_item->set_visible(true);
+ _shape_item->set_visible(false);
+ _simplify->set_visible(false);
+ _flatten_spiro_bspline->set_visible(false);
+ _flatten_simplify->set_visible(false);
+ for (auto button : _mode_buttons) {
+ button->set_sensitive(false);
+ }
+ } else {
+ guint freehandMode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+
+ _minpressure->set_visible(false);
+ _maxpressure->set_visible(false);
+ _cap_item->set_visible(false);
+ _shape_item->set_visible(true);
+ bool simplify_visible = freehandMode != 2;
+ _simplify->set_visible(simplify_visible);
+ _flatten_simplify->set_visible(simplify_visible && _simplify->get_active());
+ if (freehandMode == 1 || freehandMode == 2) {
+ _flatten_spiro_bspline->set_visible(true);
+ }
+ for (auto button : _mode_buttons) {
+ button->set_sensitive(true);
+ }
+ }
+}
+
+void
+PencilToolbar::add_advanced_shape_options(bool tool_is_pencil)
+{
+ /*advanced shape options */
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ std::vector<gchar*> freehand_shape_dropdown_items_list = {
+ const_cast<gchar *>(C_("Freehand shape", "None")),
+ _("Triangle in"),
+ _("Triangle out"),
+ _("Ellipse"),
+ _("From clipboard"),
+ _("Bend from clipboard"),
+ _("Last applied")
+ };
+
+ for (auto item:freehand_shape_dropdown_items_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _shape_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Shape:"), _("Shape of new paths drawn by this tool"), "Not Used", store));
+ _shape_item->use_group_label(true);
+
+ auto prefs = Inkscape::Preferences::get();
+ int shape = prefs->getInt((tool_is_pencil ?
+ "/tools/freehand/pencil/shape" :
+ "/tools/freehand/pen/shape" ), 0);
+ _shape_item->set_active(shape);
+
+ _shape_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_shape));
+ add(*_shape_item);
+}
+
+void
+PencilToolbar::change_shape(int shape) {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(freehand_tool_name() + "/shape", shape);
+}
+
+void PencilToolbar::add_powerstroke_cap(bool tool_is_pencil)
+{
+ /* Powerstroke cap */
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ std::vector<gchar *> powerstroke_cap_items_list = { const_cast<gchar *>(C_("Cap", "Butt")), _("Square"), _("Round"),
+ _("Peak"), _("Zero width") };
+ for (auto item : powerstroke_cap_items_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _cap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Caps:"), _("Cap for powerstroke pressure"), "Not Used", store));
+
+ auto prefs = Inkscape::Preferences::get();
+
+ int cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2);
+ _cap_item->set_active(cap);
+ _cap_item->use_group_label(true);
+
+ _cap_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_cap));
+
+ add(*_cap_item);
+}
+
+void PencilToolbar::change_cap(int cap)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt("/live_effects/powerstroke/powerpencilcap", cap);
+}
+
+void
+PencilToolbar::simplify_lpe()
+{
+ bool simplify = _simplify->get_active();
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(freehand_tool_name() + "/simplify", simplify);
+ _flatten_simplify->set_visible(simplify);
+}
+
+void
+PencilToolbar::simplify_flatten()
+{
+ auto selected = _desktop->getSelection()->items();
+ SPLPEItem* lpeitem = nullptr;
+ for (auto it(selected.begin()); it != selected.end(); ++it){
+ lpeitem = dynamic_cast<SPLPEItem*>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ PathEffectList lpelist = lpeitem->getEffectList();
+ PathEffectList::iterator i;
+ for (i = lpelist.begin(); i != lpelist.end(); ++i) {
+ LivePathEffectObject *lpeobj = (*i)->lpeobject;
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (dynamic_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe)) {
+ SPShape * shape = dynamic_cast<SPShape *>(lpeitem);
+ if(shape){
+ SPCurve * c = shape->getCurveForEdit();
+ lpe->doEffect(c);
+ lpeitem->setCurrentPathEffect(*i);
+ if (lpelist.size() > 1){
+ lpeitem->removeCurrentPathEffect(true);
+ shape->setCurveBeforeLPE(c);
+ } else {
+ lpeitem->removeCurrentPathEffect(false);
+ shape->setCurve(c, false);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (lpeitem) {
+ _desktop->getSelection()->remove(lpeitem->getRepr());
+ _desktop->getSelection()->add(lpeitem->getRepr());
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ }
+}
+
+void
+PencilToolbar::flatten_spiro_bspline()
+{
+ auto selected = _desktop->getSelection()->items();
+ SPLPEItem* lpeitem = nullptr;
+
+ for (auto it(selected.begin()); it != selected.end(); ++it){
+ lpeitem = dynamic_cast<SPLPEItem*>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ PathEffectList lpelist = lpeitem->getEffectList();
+ PathEffectList::iterator i;
+ for (i = lpelist.begin(); i != lpelist.end(); ++i) {
+ LivePathEffectObject *lpeobj = (*i)->lpeobject;
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe) ||
+ dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe))
+ {
+ SPShape * shape = dynamic_cast<SPShape *>(lpeitem);
+ if(shape){
+ SPCurve * c = shape->getCurveForEdit();
+ lpe->doEffect(c);
+ lpeitem->setCurrentPathEffect(*i);
+ if (lpelist.size() > 1){
+ lpeitem->removeCurrentPathEffect(true);
+ shape->setCurveBeforeLPE(c);
+ } else {
+ lpeitem->removeCurrentPathEffect(false);
+ shape->setCurve(c, false);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (lpeitem) {
+ _desktop->getSelection()->remove(lpeitem->getRepr());
+ _desktop->getSelection()->add(lpeitem->getRepr());
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ }
+}
+
+GtkWidget *
+PencilToolbar::create_pen(SPDesktop *desktop)
+{
+ auto toolbar = new PencilToolbar(desktop, false);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+PencilToolbar::tolerance_value_changed()
+{
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _freeze = true;
+ prefs->setDouble("/tools/freehand/pencil/tolerance",
+ _tolerance_adj->get_value());
+ _freeze = false;
+ auto selected = _desktop->getSelection()->items();
+ for (auto it(selected.begin()); it != selected.end(); ++it){
+ SPLPEItem* lpeitem = dynamic_cast<SPLPEItem*>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect* simplify = lpeitem->getPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY);
+ if(simplify){
+ Inkscape::LivePathEffect::LPESimplify *lpe_simplify = dynamic_cast<Inkscape::LivePathEffect::LPESimplify*>(simplify->getLPEObj()->get_lpe());
+ if (lpe_simplify) {
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0);
+ tol = tol/(100.0*(102.0-tol));
+ std::ostringstream ss;
+ ss << tol;
+ Inkscape::LivePathEffect::Effect* powerstroke = lpeitem->getPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE);
+ bool simplified = false;
+ if(powerstroke){
+ Inkscape::LivePathEffect::LPEPowerStroke *lpe_powerstroke = dynamic_cast<Inkscape::LivePathEffect::LPEPowerStroke*>(powerstroke->getLPEObj()->get_lpe());
+ if(lpe_powerstroke){
+ lpe_powerstroke->getRepr()->setAttribute("is_visible", "false");
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ SPShape *sp_shape = dynamic_cast<SPShape *>(lpeitem);
+ if (sp_shape) {
+ guint previous_curve_length = sp_shape->getCurve(true)->get_segment_count();
+ lpe_simplify->getRepr()->setAttribute("threshold", ss.str());
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ simplified = true;
+ guint curve_length = sp_shape->getCurve(true)->get_segment_count();
+ std::vector<Geom::Point> ts = lpe_powerstroke->offset_points.data();
+ double factor = (double)curve_length/ (double)previous_curve_length;
+ for (auto & t : ts) {
+ t[Geom::X] = t[Geom::X] * factor;
+ }
+ lpe_powerstroke->offset_points.param_setValue(ts);
+ }
+ lpe_powerstroke->getRepr()->setAttribute("is_visible", "true");
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ }
+ }
+ if(!simplified){
+ lpe_simplify->getRepr()->setAttribute("threshold", ss.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/toolbar/pencil-toolbar.h b/src/ui/toolbar/pencil-toolbar.h
new file mode 100644
index 0000000..2b2f676
--- /dev/null
+++ b/src/ui/toolbar/pencil-toolbar.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PENCIL_TOOLBAR_H
+#define SEEN_PENCIL_TOOLBAR_H
+
+/**
+ * @file
+ * Pencil aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+class SpinButtonToolItem;
+class ComboToolItem;
+}
+
+namespace Toolbar {
+class PencilToolbar : public Toolbar {
+private:
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+
+ Gtk::ToggleToolButton *_pressure_item;
+ UI::Widget::SpinButtonToolItem *_minpressure;
+ UI::Widget::SpinButtonToolItem *_maxpressure;
+
+ XML::Node *_repr;
+ Gtk::ToolButton *_flatten_spiro_bspline;
+ Gtk::ToolButton *_flatten_simplify;
+
+ UI::Widget::ComboToolItem *_shape_item;
+ UI::Widget::ComboToolItem *_cap_item;
+
+ Gtk::ToggleToolButton *_simplify;
+
+ bool _freeze;
+
+ Glib::RefPtr<Gtk::Adjustment> _minpressure_adj;
+ Glib::RefPtr<Gtk::Adjustment> _maxpressure_adj;
+ Glib::RefPtr<Gtk::Adjustment> _tolerance_adj;
+
+ void add_freehand_mode_toggle(bool tool_is_pencil);
+ void mode_changed(int mode);
+ Glib::ustring const freehand_tool_name();
+ void minpressure_value_changed();
+ void maxpressure_value_changed();
+ void use_pencil_pressure();
+ void tolerance_value_changed();
+ void add_advanced_shape_options(bool tool_is_pencil);
+ void add_powerstroke_cap(bool tool_is_pencil);
+ void change_shape(int shape);
+ void change_cap(int cap);
+ void simplify_lpe();
+ void simplify_flatten();
+ void flatten_spiro_bspline();
+
+protected:
+ PencilToolbar(SPDesktop *desktop, bool pencil_mode);
+ ~PencilToolbar() override;
+
+public:
+ static GtkWidget * create_pencil(SPDesktop *desktop);
+ static GtkWidget * create_pen(SPDesktop *desktop);
+};
+}
+}
+}
+
+#endif /* !SEEN_PENCIL_TOOLBAR_H */
diff --git a/src/ui/toolbar/rect-toolbar.cpp b/src/ui/toolbar/rect-toolbar.cpp
new file mode 100644
index 0000000..c938bb9
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.cpp
@@ -0,0 +1,407 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Rect aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "rect-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/separatortoolitem.h>
+#include <gtkmm/toolbutton.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-rect.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+#include "widgets/widget-sizes.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+static Inkscape::XML::NodeEventVector rect_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::RectToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+RectToolbar::RectToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _freeze(false),
+ _single(true),
+ _repr(nullptr),
+ _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))))
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // rx/ry units menu: create
+ //tracker->addUnit( SP_UNIT_PERCENT, 0 );
+ // fixme: add % meaning per cent of the width/height
+ _tracker->setActiveUnit(desktop->getNamedView()->display_units);
+ _mode_item->set_use_markup(true);
+
+ /* W */
+ {
+ auto width_val = prefs->getDouble("/tools/shapes/rect/width", 0);
+ _width_adj = Gtk::Adjustment::create(width_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-width", _("W:"), _width_adj));
+ _width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ _width_item->set_all_tooltip_text(_("Width of rectangle"));
+
+ _width_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed),
+ _width_adj,
+ "width",
+ &SPRect::setVisibleWidth));
+ _tracker->addAdjustment(_width_adj->gobj());
+ _width_item->set_sensitive(false);
+
+ std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ _width_item->set_custom_numeric_menu_data(values);
+ }
+
+ /* H */
+ {
+ auto height_val = prefs->getDouble("/tools/shapes/rect/height", 0);
+
+ _height_adj = Gtk::Adjustment::create(height_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _height_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed),
+ _height_adj,
+ "height",
+ &SPRect::setVisibleHeight));
+ _tracker->addAdjustment(_height_adj->gobj());
+
+ std::vector<double> values = { 1, 2, 3, 5, 10, 20, 50, 100, 200, 500};
+ _height_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-height", _("H:"), _height_adj));
+ _height_item->set_custom_numeric_menu_data(values);
+ _height_item->set_all_tooltip_text(_("Height of rectangle"));
+ _height_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _height_item->set_sensitive(false);
+ }
+
+ /* rx */
+ {
+ std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""};
+ std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100};
+ auto rx_val = prefs->getDouble("/tools/shapes/rect/rx", 0);
+ _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed),
+ _rx_adj,
+ "rx",
+ &SPRect::setVisibleRx));
+ _tracker->addAdjustment(_rx_adj->gobj());
+ _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-rx", _("Rx:"), _rx_adj));
+ _rx_item->set_all_tooltip_text(_("Horizontal radius of rounded corners"));
+ _rx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ _rx_item->set_custom_numeric_menu_data(values, labels);
+ }
+
+ /* ry */
+ {
+ std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""};
+ std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100};
+ auto ry_val = prefs->getDouble("/tools/shapes/rect/ry", 0);
+ _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed),
+ _ry_adj,
+ "ry",
+ &SPRect::setVisibleRy));
+ _tracker->addAdjustment(_ry_adj->gobj());
+ _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-ry", _("Ry:"), _ry_adj));
+ _ry_item->set_all_tooltip_text(_("Vertical radius of rounded corners"));
+ _ry_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ _ry_item->set_custom_numeric_menu_data(values, labels);
+ }
+
+ // add the units menu
+ auto unit_menu_ti = _tracker->create_tool_item(_("Units"), (""));
+
+ /* Reset */
+ {
+ _not_rounded = Gtk::manage(new Gtk::ToolButton(_("Not rounded")));
+ _not_rounded->set_tooltip_text(_("Make corners sharp"));
+ _not_rounded->set_icon_name(INKSCAPE_ICON("rectangle-make-corners-sharp"));
+ _not_rounded->signal_clicked().connect(sigc::mem_fun(*this, &RectToolbar::defaults));
+ _not_rounded->set_sensitive(true);
+ }
+
+ add(*_mode_item);
+ add(*_width_item);
+ add(*_height_item);
+ add(*_rx_item);
+ add(*_ry_item);
+ add(*unit_menu_ti);
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+ add(*_not_rounded);
+ show_all();
+
+ sensitivize();
+
+ _desktop->connectEventContextChanged(sigc::mem_fun(*this, &RectToolbar::watch_ec));
+}
+
+RectToolbar::~RectToolbar()
+{
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+GtkWidget *
+RectToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new RectToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+RectToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ void (SPRect::*setter)(gdouble))
+{
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/shapes/rect/") + value_name,
+ Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze || _tracker->isUpdating()) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ if (SP_IS_RECT(*i)) {
+ if (adj->get_value() != 0) {
+ (SP_RECT(*i)->*setter)(Quantity::convert(adj->get_value(), unit, "px"));
+ } else {
+ (*i)->removeAttribute(value_name);
+ }
+ modmade = true;
+ }
+ }
+
+ sensitivize();
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_RECT,
+ _("Change rectangle"));
+ }
+
+ _freeze = false;
+}
+
+void
+RectToolbar::sensitivize()
+{
+ if (_rx_adj->get_value() == 0 && _ry_adj->get_value() == 0 && _single) { // only for a single selected rect (for now)
+ _not_rounded->set_sensitive(false);
+ } else {
+ _not_rounded->set_sensitive(true);
+ }
+}
+
+void
+RectToolbar::defaults()
+{
+ _rx_adj->set_value(0.0);
+ _ry_adj->set_value(0.0);
+
+ sensitivize();
+}
+
+void
+RectToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ static sigc::connection changed;
+
+ // use of dynamic_cast<> seems wrong here -- we just need to check the current tool
+
+ if (dynamic_cast<Inkscape::UI::Tools::RectTool *>(ec)) {
+ Inkscape::Selection *sel = desktop->getSelection();
+
+ changed = sel->connectChanged(sigc::mem_fun(*this, &RectToolbar::selection_changed));
+
+ // Synthesize an emission to trigger the update
+ selection_changed(sel);
+ } else {
+ if (changed) {
+ changed.disconnect();
+
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+ }
+}
+
+/**
+ * \param selection should not be NULL.
+ */
+void
+RectToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+ SPItem *item = nullptr;
+
+ if (_repr) { // remove old listener
+ _item = nullptr;
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ if (SP_IS_RECT(*i)) {
+ n_selected++;
+ item = *i;
+ repr = item->getRepr();
+ }
+ }
+
+ _single = false;
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ _width_item->set_sensitive(false);
+ _height_item->set_sensitive(false);
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ _single = true;
+ _width_item->set_sensitive(true);
+ _height_item->set_sensitive(true);
+
+ if (repr) {
+ _repr = repr;
+ _item = item;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&rect_tb_repr_events, this);
+ _repr->synthesizeEvents(&rect_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ sensitivize();
+ }
+}
+
+void RectToolbar::event_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*name*/,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer data)
+{
+ auto toolbar = reinterpret_cast<RectToolbar*>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ Unit const *unit = toolbar->_tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ if (toolbar->_item && SP_IS_RECT(toolbar->_item)) {
+ {
+ gdouble rx = SP_RECT(toolbar->_item)->getVisibleRx();
+ toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit));
+ }
+
+ {
+ gdouble ry = SP_RECT(toolbar->_item)->getVisibleRy();
+ toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit));
+ }
+
+ {
+ gdouble width = SP_RECT(toolbar->_item)->getVisibleWidth();
+ toolbar->_width_adj->set_value(Quantity::convert(width, "px", unit));
+ }
+
+ {
+ gdouble height = SP_RECT(toolbar->_item)->getVisibleHeight();
+ toolbar->_height_adj->set_value(Quantity::convert(height, "px", unit));
+ }
+ }
+
+ toolbar->sensitivize();
+ toolbar->_freeze = 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 :
diff --git a/src/ui/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h
new file mode 100644
index 0000000..58d4b2c
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.h
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_RECT_TOOLBAR_H
+#define SEEN_RECT_TOOLBAR_H
+
+/**
+ * @file
+ * Rect aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPItem;
+class SPRect;
+
+namespace Gtk {
+class Toolbutton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class RectToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+
+ XML::Node *_repr;
+ SPItem *_item;
+
+ UI::Widget::LabelToolItem *_mode_item;
+ UI::Widget::SpinButtonToolItem *_width_item;
+ UI::Widget::SpinButtonToolItem *_height_item;
+ UI::Widget::SpinButtonToolItem *_rx_item;
+ UI::Widget::SpinButtonToolItem *_ry_item;
+ Gtk::ToolButton *_not_rounded;
+
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _height_adj;
+ Glib::RefPtr<Gtk::Adjustment> _rx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _ry_adj;
+
+ bool _freeze;
+ bool _single;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ void (SPRect::*setter)(gdouble));
+
+ void sensitivize();
+ void defaults();
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+protected:
+ RectToolbar(SPDesktop *desktop);
+ ~RectToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+
+};
+
+}
+}
+}
+
+#endif /* !SEEN_RECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp
new file mode 100644
index 0000000..87682b1
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.cpp
@@ -0,0 +1,508 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Selector aux toolbar
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2003-2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "select-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include <2geom/rect.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "verbs.h"
+
+#include "display/sp-canvas.h"
+
+#include "object/sp-item-transform.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+#include "widgets/widget-sizes.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+SelectToolbar::SelectToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _update(false),
+ _lock_btn(Gtk::manage(new Gtk::ToggleToolButton())),
+ _transform_stroke_btn(Gtk::manage(new Gtk::ToggleToolButton())),
+ _transform_corners_btn(Gtk::manage(new Gtk::ToggleToolButton())),
+ _transform_gradient_btn(Gtk::manage(new Gtk::ToggleToolButton())),
+ _transform_pattern_btn(Gtk::manage(new Gtk::ToggleToolButton()))
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ add_toolbutton_for_verb(SP_VERB_EDIT_SELECT_ALL);
+ add_toolbutton_for_verb(SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS);
+ auto deselect_button = add_toolbutton_for_verb(SP_VERB_EDIT_DESELECT);
+ _context_items.push_back(deselect_button);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ auto object_rotate_90_ccw_button = add_toolbutton_for_verb(SP_VERB_OBJECT_ROTATE_90_CCW);
+ _context_items.push_back(object_rotate_90_ccw_button);
+
+ auto object_rotate_90_cw_button = add_toolbutton_for_verb(SP_VERB_OBJECT_ROTATE_90_CW);
+ _context_items.push_back(object_rotate_90_cw_button);
+
+ auto object_flip_horizontal_button = add_toolbutton_for_verb(SP_VERB_OBJECT_FLIP_HORIZONTAL);
+ _context_items.push_back(object_flip_horizontal_button);
+
+ auto object_flip_vertical_button = add_toolbutton_for_verb(SP_VERB_OBJECT_FLIP_VERTICAL);
+ _context_items.push_back(object_flip_vertical_button);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ auto selection_to_back_button = add_toolbutton_for_verb(SP_VERB_SELECTION_TO_BACK);
+ _context_items.push_back(selection_to_back_button);
+
+ auto selection_lower_button = add_toolbutton_for_verb(SP_VERB_SELECTION_LOWER);
+ _context_items.push_back(selection_lower_button);
+
+ auto selection_raise_button = add_toolbutton_for_verb(SP_VERB_SELECTION_RAISE);
+ _context_items.push_back(selection_raise_button);
+
+ auto selection_to_front_button = add_toolbutton_for_verb(SP_VERB_SELECTION_TO_FRONT);
+ _context_items.push_back(selection_to_front_button);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ _tracker->addUnit(unit_table.getUnit("%"));
+ _tracker->setActiveUnit( desktop->getNamedView()->display_units );
+
+ // x-value control
+ auto x_val = prefs->getDouble("/tools/select/X", 0.0);
+ _adj_x = Gtk::Adjustment::create(x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _adj_x->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_x));
+ _tracker->addAdjustment(_adj_x->gobj());
+
+ auto x_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-x",
+ C_("Select toolbar", "X:"),
+ _adj_x,
+ SPIN_STEP, 3));
+ x_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ x_btn->set_all_tooltip_text(C_("Select toolbar", "Horizontal coordinate of selection"));
+ _context_items.push_back(x_btn);
+ add(*x_btn);
+
+ // y-value control
+ auto y_val = prefs->getDouble("/tools/select/Y", 0.0);
+ _adj_y = Gtk::Adjustment::create(y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _adj_y->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_y));
+ _tracker->addAdjustment(_adj_y->gobj());
+
+ auto y_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-y",
+ C_("Select toolbar", "Y:"),
+ _adj_y,
+ SPIN_STEP, 3));
+ y_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ y_btn->set_all_tooltip_text(C_("Select toolbar", "Vertical coordinate of selection"));
+ _context_items.push_back(y_btn);
+ add(*y_btn);
+
+ // width-value control
+ auto w_val = prefs->getDouble("/tools/select/width", 0.0);
+ _adj_w = Gtk::Adjustment::create(w_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _adj_w->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_w));
+ _tracker->addAdjustment(_adj_w->gobj());
+
+ auto w_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-width",
+ C_("Select toolbar", "W:"),
+ _adj_w,
+ SPIN_STEP, 3));
+ w_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ w_btn->set_all_tooltip_text(C_("Select toolbar", "Width of selection"));
+ _context_items.push_back(w_btn);
+ add(*w_btn);
+
+ // lock toggle
+ _lock_btn->set_label(_("Lock width and height"));
+ _lock_btn->set_tooltip_text(_("When locked, change both width and height by the same proportion"));
+ _lock_btn->set_icon_name(INKSCAPE_ICON("object-unlocked"));
+ _lock_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_lock));
+ set_data("lock", _lock_btn->gobj());
+ add(*_lock_btn);
+
+ // height-value control
+ auto h_val = prefs->getDouble("/tools/select/height", 0.0);
+ _adj_h = Gtk::Adjustment::create(h_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP);
+ _adj_h->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_h));
+ _tracker->addAdjustment(_adj_h->gobj());
+
+ auto h_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-height",
+ C_("Select toolbar", "H:"),
+ _adj_h,
+ SPIN_STEP, 3));
+ h_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas)));
+ h_btn->set_all_tooltip_text(C_("Select toolbar", "Height of selection"));
+ _context_items.push_back(h_btn);
+ add(*h_btn);
+
+ // units menu
+ auto unit_menu = _tracker->create_tool_item(_("Units"), ("") );
+ add(*unit_menu);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ _transform_stroke_btn->set_label(_("Scale stroke width"));
+ _transform_stroke_btn->set_tooltip_text(_("When scaling objects, scale the stroke width by the same proportion"));
+ _transform_stroke_btn->set_icon_name(INKSCAPE_ICON("transform-affect-stroke"));
+ _transform_stroke_btn->set_active(prefs->getBool("/options/transform/stroke", true));
+ _transform_stroke_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_stroke));
+ add(*_transform_stroke_btn);
+
+ _transform_corners_btn->set_label(_("Scale rounded corners"));
+ _transform_corners_btn->set_tooltip_text(_("When scaling rectangles, scale the radii of rounded corners"));
+ _transform_corners_btn->set_icon_name(INKSCAPE_ICON("transform-affect-rounded-corners"));
+ _transform_corners_btn->set_active(prefs->getBool("/options/transform/rectcorners", true));
+ _transform_corners_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_corners));
+ add(*_transform_corners_btn);
+
+ _transform_gradient_btn->set_label(_("Move gradients"));
+ _transform_gradient_btn->set_tooltip_text(_("Move gradients (in fill or stroke) along with the objects"));
+ _transform_gradient_btn->set_icon_name(INKSCAPE_ICON("transform-affect-gradient"));
+ _transform_gradient_btn->set_active(prefs->getBool("/options/transform/gradient", true));
+ _transform_gradient_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_gradient));
+ add(*_transform_gradient_btn);
+
+ _transform_pattern_btn->set_label(_("Move patterns"));
+ _transform_pattern_btn->set_tooltip_text(_("Move patterns (in fill or stroke) along with the objects"));
+ _transform_pattern_btn->set_icon_name(INKSCAPE_ICON("transform-affect-pattern"));
+ _transform_pattern_btn->set_active(prefs->getBool("/options/transform/pattern", true));
+ _transform_pattern_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_pattern));
+ add(*_transform_pattern_btn);
+
+ // Force update when selection changes.
+ INKSCAPE.signal_selection_modified.connect(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_modified));
+ INKSCAPE.signal_selection_changed.connect (sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_changed));
+
+ // Update now.
+ layout_widget_update(SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->getSelection() : nullptr);
+
+ for (auto item : _context_items) {
+ if ( item->is_sensitive() ) {
+ item->set_sensitive(false);
+ }
+ }
+
+ show_all();
+}
+
+GtkWidget *
+SelectToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new SelectToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+SelectToolbar::any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj)
+{
+ if (_update) {
+ return;
+ }
+
+ if ( !_tracker || _tracker->isUpdating() ) {
+ /*
+ * When only units are being changed, don't treat changes
+ * to adjuster values as object changes.
+ */
+ return;
+ }
+ _update = true;
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+
+ document->ensureUpToDate ();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ Geom::OptRect bbox_vis = selection->visualBounds();
+ Geom::OptRect bbox_geom = selection->geometricBounds();
+
+ int prefs_bbox = prefs->getInt("/tools/bounding_box");
+ SPItem::BBoxType bbox_type = (prefs_bbox == 0)?
+ SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX;
+ Geom::OptRect bbox_user = selection->bounds(bbox_type);
+
+ if ( !bbox_user ) {
+ _update = false;
+ return;
+ }
+
+ gdouble x0 = 0;
+ gdouble y0 = 0;
+ gdouble x1 = 0;
+ gdouble y1 = 0;
+ gdouble xrel = 0;
+ gdouble yrel = 0;
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ x0 = Quantity::convert(_adj_x->get_value(), unit, "px");
+ y0 = Quantity::convert(_adj_y->get_value(), unit, "px");
+ x1 = x0 + Quantity::convert(_adj_w->get_value(), unit, "px");
+ xrel = Quantity::convert(_adj_w->get_value(), unit, "px") / bbox_user->dimensions()[Geom::X];
+ y1 = y0 + Quantity::convert(_adj_h->get_value(), unit, "px");;
+ yrel = Quantity::convert(_adj_h->get_value(), unit, "px") / bbox_user->dimensions()[Geom::Y];
+ } else {
+ double const x0_propn = _adj_x->get_value() / 100 / unit->factor;
+ x0 = bbox_user->min()[Geom::X] * x0_propn;
+ double const y0_propn = _adj_y->get_value() / 100 / unit->factor;
+ y0 = y0_propn * bbox_user->min()[Geom::Y];
+ xrel = _adj_w->get_value() / (100 / unit->factor);
+ x1 = x0 + xrel * bbox_user->dimensions()[Geom::X];
+ yrel = _adj_h->get_value() / (100 / unit->factor);
+ y1 = y0 + yrel * bbox_user->dimensions()[Geom::Y];
+ }
+
+ // Keep proportions if lock is on
+ if ( _lock_btn->get_active() ) {
+ if (adj == _adj_h) {
+ x1 = x0 + yrel * bbox_user->dimensions()[Geom::X];
+ } else if (adj == _adj_w) {
+ y1 = y0 + xrel * bbox_user->dimensions()[Geom::Y];
+ }
+ }
+
+ // scales and moves, in px
+ double mh = fabs(x0 - bbox_user->min()[Geom::X]);
+ double sh = fabs(x1 - bbox_user->max()[Geom::X]);
+ double mv = fabs(y0 - bbox_user->min()[Geom::Y]);
+ double sv = fabs(y1 - bbox_user->max()[Geom::Y]);
+
+ // unless the unit is %, convert the scales and moves to the unit
+ if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ mh = Quantity::convert(mh, "px", unit);
+ sh = Quantity::convert(sh, "px", unit);
+ mv = Quantity::convert(mv, "px", unit);
+ sv = Quantity::convert(sv, "px", unit);
+ }
+
+ // do the action only if one of the scales/moves is greater than half the last significant
+ // digit in the spinbox (currently spinboxes have 3 fractional digits, so that makes 0.0005). If
+ // the value was changed by the user, the difference will be at least that much; otherwise it's
+ // just rounding difference between the spinbox value and actual value, so no action is
+ // performed
+ char const * const actionkey = ( mh > 5e-4 ? "selector:toolbar:move:horizontal" :
+ sh > 5e-4 ? "selector:toolbar:scale:horizontal" :
+ mv > 5e-4 ? "selector:toolbar:move:vertical" :
+ sv > 5e-4 ? "selector:toolbar:scale:vertical" : nullptr );
+
+ if (actionkey != nullptr) {
+
+ // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed
+ desktop->getCanvas()->forceFullRedrawAfterInterruptions(0);
+
+ bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
+ bool preserve = prefs->getBool("/options/preservetransform/value", false);
+
+ Geom::Affine scaler;
+ if (bbox_type == SPItem::VISUAL_BBOX) {
+ scaler = get_scale_transform_for_variable_stroke (*bbox_vis, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
+ } else {
+ // 1) We could have use the newer get_scale_transform_for_variable_stroke() here, but to avoid regressions
+ // we'll just use the old get_scale_transform_for_uniform_stroke() for now.
+ // 2) get_scale_transform_for_uniform_stroke() is intended for visual bounding boxes, not geometrical ones!
+ // we'll trick it into using a geometric bounding box though, by setting the stroke width to zero
+ scaler = get_scale_transform_for_uniform_stroke (*bbox_geom, 0, 0, false, false, x0, y0, x1, y1);
+ }
+
+ selection->applyAffine(scaler);
+ DocumentUndo::maybeDone(document, actionkey, SP_VERB_CONTEXT_SELECT,
+ _("Transform by toolbar"));
+
+ // resume interruptibility
+ desktop->getCanvas()->endForcedFullRedraws();
+ }
+
+ _update = false;
+}
+
+void
+SelectToolbar::layout_widget_update(Inkscape::Selection *sel)
+{
+ if (_update) {
+ return;
+ }
+
+ _update = true;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ using Geom::X;
+ using Geom::Y;
+ if ( sel && !sel->isEmpty() ) {
+ int prefs_bbox = prefs->getInt("/tools/bounding_box", 0);
+ SPItem::BBoxType bbox_type = (prefs_bbox ==0)?
+ SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX;
+ Geom::OptRect const bbox(sel->bounds(bbox_type));
+ if ( bbox ) {
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ struct { char const *key; double val; } const keyval[] = {
+ { "X", bbox->min()[X] },
+ { "Y", bbox->min()[Y] },
+ { "width", bbox->dimensions()[X] },
+ { "height", bbox->dimensions()[Y] }
+ };
+
+ if (unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ double const val = unit->factor * 100;
+ _adj_x->set_value(val);
+ _adj_y->set_value(val);
+ _adj_w->set_value(val);
+ _adj_h->set_value(val);
+ _tracker->setFullVal( _adj_x->gobj(), keyval[0].val );
+ _tracker->setFullVal( _adj_y->gobj(), keyval[1].val );
+ _tracker->setFullVal( _adj_w->gobj(), keyval[2].val );
+ _tracker->setFullVal( _adj_h->gobj(), keyval[3].val );
+ } else {
+ _adj_x->set_value(Quantity::convert(keyval[0].val, "px", unit));
+ _adj_y->set_value(Quantity::convert(keyval[1].val, "px", unit));
+ _adj_w->set_value(Quantity::convert(keyval[2].val, "px", unit));
+ _adj_h->set_value(Quantity::convert(keyval[3].val, "px", unit));
+ }
+ }
+ }
+
+ _update = false;
+}
+
+void
+SelectToolbar::on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags)
+{
+ if ((_desktop->getSelection() == selection) // only respond to changes in our desktop
+ && (flags & (SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_CHILD_MODIFIED_FLAG )))
+ {
+ layout_widget_update(selection);
+ }
+}
+
+void
+SelectToolbar::on_inkscape_selection_changed(Inkscape::Selection *selection)
+{
+ if (_desktop->getSelection() == selection) { // only respond to changes in our desktop
+ bool setActive = (selection && !selection->isEmpty());
+
+ for (auto item : _context_items) {
+ if ( setActive != item->get_sensitive() ) {
+ item->set_sensitive(setActive);
+ }
+ }
+
+ layout_widget_update(selection);
+ }
+}
+
+void
+SelectToolbar::toggle_lock() {
+ if ( _lock_btn->get_active() ) {
+ _lock_btn->set_icon_name(INKSCAPE_ICON("object-locked"));
+ } else {
+ _lock_btn->set_icon_name(INKSCAPE_ICON("object-unlocked"));
+ }
+}
+
+void
+SelectToolbar::toggle_stroke()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool active = _transform_stroke_btn->get_active();
+ prefs->setBool("/options/transform/stroke", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>scaled</b> when objects are scaled."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>not scaled</b> when objects are scaled."));
+ }
+}
+
+void
+SelectToolbar::toggle_corners()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool active = _transform_corners_btn->get_active();
+ prefs->setBool("/options/transform/rectcorners", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>scaled</b> when rectangles are scaled."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>not scaled</b> when rectangles are scaled."));
+ }
+}
+
+void
+SelectToolbar::toggle_gradient()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool active = _transform_gradient_btn->get_active();
+ prefs->setBool("/options/transform/gradient", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed)."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed)."));
+ }
+}
+
+void
+SelectToolbar::toggle_pattern()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool active = _transform_pattern_btn->get_active();
+ prefs->setInt("/options/transform/pattern", active);
+ if ( active ) {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed)."));
+ } else {
+ _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed)."));
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/select-toolbar.h b/src/ui/toolbar/select-toolbar.h
new file mode 100644
index 0000000..79c3310
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.h
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SELECT_TOOLBAR_H
+#define SEEN_SELECT_TOOLBAR_H
+
+/** \file
+ * Selector aux toolbar
+ */
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <bulia@dr.com>
+ *
+ * Copyright (C) 2003 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+
+namespace Widget {
+class UnitTracker;
+}
+
+namespace Toolbar {
+
+class SelectToolbar : public Toolbar {
+private:
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+
+ Glib::RefPtr<Gtk::Adjustment> _adj_x;
+ Glib::RefPtr<Gtk::Adjustment> _adj_y;
+ Glib::RefPtr<Gtk::Adjustment> _adj_w;
+ Glib::RefPtr<Gtk::Adjustment> _adj_h;
+ Gtk::ToggleToolButton *_lock_btn;
+ Gtk::ToggleToolButton *_transform_stroke_btn;
+ Gtk::ToggleToolButton *_transform_corners_btn;
+ Gtk::ToggleToolButton *_transform_gradient_btn;
+ Gtk::ToggleToolButton *_transform_pattern_btn;
+
+ std::vector<Gtk::ToolItem *> _context_items;
+
+ bool _update;
+
+ void any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj);
+ void layout_widget_update(Inkscape::Selection *sel);
+ void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags);
+ void on_inkscape_selection_changed(Inkscape::Selection *selection);
+ void toggle_lock();
+ void toggle_stroke();
+ void toggle_corners();
+ void toggle_gradient();
+ void toggle_pattern();
+
+protected:
+ SelectToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+#endif /* !SEEN_SELECT_TOOLBAR_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/toolbar/snap-toolbar.cpp b/src/ui/toolbar/snap-toolbar.cpp
new file mode 100644
index 0000000..483faf3
--- /dev/null
+++ b/src/ui/toolbar/snap-toolbar.cpp
@@ -0,0 +1,402 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/**
+ * @file
+ * Inkscape Snap toolbar
+ *
+ * @authors Inkscape Authors
+ * Copyright (C) 1999-2019 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include "snap-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include "attributes.h"
+#include "desktop.h"
+#include "verbs.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+SnapToolbar::SnapToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _freeze(false)
+{
+ // Global snapping control
+ {
+ auto snap_global_verb = Inkscape::Verb::get(SP_VERB_TOGGLE_SNAPPING);
+ _snap_global_item = add_toggle_button(snap_global_verb->get_name(),
+ snap_global_verb->get_tip());
+ _snap_global_item->set_icon_name(INKSCAPE_ICON("snap"));
+ _snap_global_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_GLOBAL));
+ }
+
+ add_separator();
+
+ // Snapping to bounding boxes
+ {
+ _snap_from_bbox_corner_item = add_toggle_button(_("Bounding box"),
+ _("Snap bounding boxes"));
+ _snap_from_bbox_corner_item->set_icon_name(INKSCAPE_ICON("snap"));
+ _snap_from_bbox_corner_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_BBOX));
+ }
+
+ {
+ _snap_to_bbox_path_item = add_toggle_button(_("Bounding box edges"),
+ _("Snap to edges of a bounding box"));
+ _snap_to_bbox_path_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-edges"));
+ _snap_to_bbox_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE));
+ }
+
+ {
+ _snap_to_bbox_node_item = add_toggle_button(_("Bounding box corners"),
+ _("Snap bounding box corners"));
+ _snap_to_bbox_node_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-corners"));
+ _snap_to_bbox_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_BBOX_CORNER));
+ }
+
+ {
+ _snap_to_from_bbox_edge_midpoints_item = add_toggle_button(_("BBox Edge Midpoints"),
+ _("Snap midpoints of bounding box edges"));
+ _snap_to_from_bbox_edge_midpoints_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-midpoints"));
+ _snap_to_from_bbox_edge_midpoints_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE_MIDPOINT));
+ }
+
+ {
+ _snap_to_from_bbox_edge_centers_item = add_toggle_button(_("BBox Centers"),
+ _("Snapping centers of bounding boxes"));
+ _snap_to_from_bbox_edge_centers_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center"));
+ _snap_to_from_bbox_edge_centers_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_BBOX_MIDPOINT));
+ }
+
+ add_separator();
+
+ // Snapping to nodes, paths & handles
+ {
+ _snap_from_node_item = add_toggle_button(_("Nodes"),
+ _("Snap nodes, paths, and handles"));
+ _snap_from_node_item->set_icon_name(INKSCAPE_ICON("snap"));
+ _snap_from_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_NODE));
+ }
+
+ {
+ _snap_to_item_path_item = add_toggle_button(_("Paths"),
+ _("Snap to paths"));
+ _snap_to_item_path_item->set_icon_name(INKSCAPE_ICON("snap-nodes-path"));
+ _snap_to_item_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_PATH));
+ }
+
+ {
+ _snap_to_path_intersections_item = add_toggle_button(_("Path intersections"),
+ _("Snap to path intersections"));
+ _snap_to_path_intersections_item->set_icon_name(INKSCAPE_ICON("snap-nodes-intersection"));
+ _snap_to_path_intersections_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_PATH_INTERSECTION));
+ }
+
+ {
+ _snap_to_item_node_item = add_toggle_button(_("To nodes"),
+ _("Snap to cusp nodes, incl. rectangle corners"));
+ _snap_to_item_node_item->set_icon_name(INKSCAPE_ICON("snap-nodes-cusp"));
+ _snap_to_item_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_NODE_CUSP));
+ }
+
+ {
+ _snap_to_smooth_nodes_item = add_toggle_button(_("Smooth nodes"),
+ _("Snap smooth nodes, incl. quadrant points of ellipses"));
+ _snap_to_smooth_nodes_item->set_icon_name(INKSCAPE_ICON("snap-nodes-smooth"));
+ _snap_to_smooth_nodes_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_NODE_SMOOTH));
+ }
+
+ {
+ _snap_to_from_line_midpoints_item = add_toggle_button(_("Line Midpoints"),
+ _("Snap midpoints of line segments"));
+ _snap_to_from_line_midpoints_item->set_icon_name(INKSCAPE_ICON("snap-nodes-midpoint"));
+ _snap_to_from_line_midpoints_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_LINE_MIDPOINT));
+ }
+
+ add_separator();
+
+ {
+ _snap_from_others_item = add_toggle_button(_("Others"),
+ _("Snap other points (centers, guide origins, gradient handles, etc.)"));
+ _snap_from_others_item->set_icon_name(INKSCAPE_ICON("snap"));
+ _snap_from_others_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_OTHERS));
+ }
+
+ {
+ _snap_to_from_object_centers_item = add_toggle_button(_("Object Centers"),
+ _("Snap centers of objects"));
+ _snap_to_from_object_centers_item->set_icon_name(INKSCAPE_ICON("snap-nodes-center"));
+ _snap_to_from_object_centers_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_OBJECT_MIDPOINT));
+ }
+
+ {
+ _snap_to_from_rotation_center_item = add_toggle_button(_("Rotation Centers"),
+ _("Snap an item's rotation center"));
+ _snap_to_from_rotation_center_item->set_icon_name(INKSCAPE_ICON("snap-nodes-rotation-center"));
+ _snap_to_from_rotation_center_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_ROTATION_CENTER));
+ }
+
+ {
+ _snap_to_from_text_baseline_item = add_toggle_button(_("Text baseline"),
+ _("Snap text anchors and baselines"));
+ _snap_to_from_text_baseline_item->set_icon_name(INKSCAPE_ICON("snap-text-baseline"));
+ _snap_to_from_text_baseline_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_TEXT_BASELINE));
+ }
+
+ add_separator();
+
+ {
+ _snap_to_page_border_item = add_toggle_button(_("Page border"),
+ _("Snap to the page border"));
+ _snap_to_page_border_item->set_icon_name(INKSCAPE_ICON("snap-page"));
+ _snap_to_page_border_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_PAGE_BORDER));
+ }
+
+ {
+ _snap_to_grids_item = add_toggle_button(_("Grids"),
+ _("Snap to grids"));
+ _snap_to_grids_item->set_icon_name(INKSCAPE_ICON("grid-rectangular"));
+ _snap_to_grids_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_GRID));
+ }
+
+ {
+ _snap_to_guides_item = add_toggle_button(_("Guides"),
+ _("Snap guides"));
+ _snap_to_guides_item->set_icon_name(INKSCAPE_ICON("guides"));
+ _snap_to_guides_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled),
+ SP_ATTR_INKSCAPE_SNAP_GUIDE));
+ }
+
+ show_all();
+}
+
+GtkWidget *
+SnapToolbar::create(SPDesktop *desktop)
+{
+ auto tb = Gtk::manage(new SnapToolbar(desktop));
+ return GTK_WIDGET(tb->gobj());
+}
+
+void
+SnapToolbar::update(SnapToolbar *tb)
+{
+ auto nv = tb->_desktop->getNamedView();
+
+ if (nv == nullptr) {
+ g_warning("Namedview cannot be retrieved (in updateSnapToolbox)!");
+ return;
+ }
+
+ // The ..._set_active calls below will toggle the buttons, but this shouldn't lead to
+ // changes in our document because we're only updating the UI;
+ // Setting the "freeze" parameter to true will block the code in toggle_snap_callback()
+ tb->_freeze = true;
+
+ bool const c1 = nv->snap_manager.snapprefs.getSnapEnabledGlobally();
+ tb->_snap_global_item->set_active(c1);
+
+ bool const c2 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY);
+ tb->_snap_from_bbox_corner_item->set_active(c2);
+ tb->_snap_from_bbox_corner_item->set_sensitive(c1);
+
+ tb->_snap_to_bbox_path_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_EDGE));
+ tb->_snap_to_bbox_path_item->set_sensitive(c1 && c2);
+
+ tb->_snap_to_bbox_node_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_CORNER));
+ tb->_snap_to_bbox_node_item->set_sensitive(c1 && c2);
+
+ tb->_snap_to_from_bbox_edge_midpoints_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_EDGE_MIDPOINT));
+ tb->_snap_to_from_bbox_edge_midpoints_item->set_sensitive(c1 && c2);
+ tb->_snap_to_from_bbox_edge_centers_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_MIDPOINT));
+ tb->_snap_to_from_bbox_edge_centers_item->set_sensitive(c1 && c2);
+
+ bool const c3 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_NODE_CATEGORY);
+ tb->_snap_from_node_item->set_active(c3);
+ tb->_snap_from_node_item->set_sensitive(c1);
+
+ tb->_snap_to_item_path_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH));
+ tb->_snap_to_item_path_item->set_sensitive(c1 && c3);
+ tb->_snap_to_path_intersections_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_INTERSECTION));
+ tb->_snap_to_path_intersections_item->set_sensitive(c1 && c3);
+ tb->_snap_to_item_node_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_CUSP));
+ tb->_snap_to_item_node_item->set_sensitive(c1 && c3);
+ tb->_snap_to_smooth_nodes_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_SMOOTH));
+ tb->_snap_to_smooth_nodes_item->set_sensitive(c1 && c3);
+ tb->_snap_to_from_line_midpoints_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_LINE_MIDPOINT));
+ tb->_snap_to_from_line_midpoints_item->set_sensitive(c1 && c3);
+
+ bool const c5 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_OTHERS_CATEGORY);
+ tb->_snap_from_others_item->set_active(c5);
+ tb->_snap_from_others_item->set_sensitive(c1);
+ tb->_snap_to_from_object_centers_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_OBJECT_MIDPOINT));
+ tb->_snap_to_from_object_centers_item->set_sensitive(c1 && c5);
+ tb->_snap_to_from_rotation_center_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_ROTATION_CENTER));
+ tb->_snap_to_from_rotation_center_item->set_sensitive(c1 && c5);
+ tb->_snap_to_from_text_baseline_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_TEXT_BASELINE));
+ tb->_snap_to_from_text_baseline_item->set_sensitive(c1 && c5);
+ tb->_snap_to_page_border_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PAGE_BORDER));
+ tb->_snap_to_page_border_item->set_sensitive(c1);
+ tb->_snap_to_grids_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GRID));
+ tb->_snap_to_grids_item->set_sensitive(c1);
+ tb->_snap_to_guides_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GUIDE));
+ tb->_snap_to_guides_item->set_sensitive(c1);
+
+ tb->_freeze = false;
+}
+
+void
+SnapToolbar::on_snap_toggled(SPAttributeEnum attr)
+{
+ if(_freeze) return;
+
+ auto dt = _desktop;
+ auto nv = dt->getNamedView();
+
+ if(!nv) {
+ g_warning("No namedview specified in toggle-snap callback");
+ return;
+ }
+
+ auto doc = nv->document;
+ auto repr = nv->getRepr();
+
+ if(!repr) {
+ g_warning("This namedview doesn't have an XML representation attached!");
+ return;
+ }
+
+ DocumentUndo::ScopedInsensitive _no_undo(doc);
+
+ bool v = false;
+
+ switch (attr) {
+ case SP_ATTR_INKSCAPE_SNAP_GLOBAL:
+ dt->toggleSnapGlobal();
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_BBOX:
+ v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY);
+ sp_repr_set_boolean(repr, "inkscape:snap-bbox", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_EDGE);
+ sp_repr_set_boolean(repr, "inkscape:bbox-paths", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_BBOX_CORNER:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_CORNER);
+ sp_repr_set_boolean(repr, "inkscape:bbox-nodes", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_NODE:
+ v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_NODE_CATEGORY);
+ sp_repr_set_boolean(repr, "inkscape:snap-nodes", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_PATH:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH);
+ sp_repr_set_boolean(repr, "inkscape:object-paths", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_PATH_CLIP:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP);
+ sp_repr_set_boolean(repr, "inkscape:snap-path-clip", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_PATH_MASK:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK);
+ sp_repr_set_boolean(repr, "inkscape:snap-path-mask", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_NODE_CUSP:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_CUSP);
+ sp_repr_set_boolean(repr, "inkscape:object-nodes", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_NODE_SMOOTH:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_SMOOTH);
+ sp_repr_set_boolean(repr, "inkscape:snap-smooth-nodes", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_PATH_INTERSECTION:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_INTERSECTION);
+ sp_repr_set_boolean(repr, "inkscape:snap-intersection-paths", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_OTHERS:
+ v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_OTHERS_CATEGORY);
+ sp_repr_set_boolean(repr, "inkscape:snap-others", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_ROTATION_CENTER:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_ROTATION_CENTER);
+ sp_repr_set_boolean(repr, "inkscape:snap-center", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_GRID:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GRID);
+ sp_repr_set_boolean(repr, "inkscape:snap-grids", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_GUIDE:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GUIDE);
+ sp_repr_set_boolean(repr, "inkscape:snap-to-guides", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_PAGE_BORDER:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PAGE_BORDER);
+ sp_repr_set_boolean(repr, "inkscape:snap-page", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_LINE_MIDPOINT:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_LINE_MIDPOINT);
+ sp_repr_set_boolean(repr, "inkscape:snap-midpoints", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_OBJECT_MIDPOINT:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_OBJECT_MIDPOINT);
+ sp_repr_set_boolean(repr, "inkscape:snap-object-midpoints", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_TEXT_BASELINE:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_TEXT_BASELINE);
+ sp_repr_set_boolean(repr, "inkscape:snap-text-baseline", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE_MIDPOINT:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_EDGE_MIDPOINT);
+ sp_repr_set_boolean(repr, "inkscape:snap-bbox-edge-midpoints", !v);
+ break;
+ case SP_ATTR_INKSCAPE_SNAP_BBOX_MIDPOINT:
+ v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_MIDPOINT);
+ sp_repr_set_boolean(repr, "inkscape:snap-bbox-midpoints", !v);
+ break;
+ default:
+ g_warning("toggle_snap_callback has been called with an ID for which no action has been defined");
+ break;
+ }
+
+ doc->setModifiedSinceSave();
+}
+
+}
+}
+}
+/*
+ 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/toolbar/snap-toolbar.h b/src/ui/toolbar/snap-toolbar.h
new file mode 100644
index 0000000..7f8528c
--- /dev/null
+++ b/src/ui/toolbar/snap-toolbar.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SNAP_TOOLBAR_H
+#define SEEN_SNAP_TOOLBAR_H
+
+/**
+ * @file
+ * Snapping toolbar
+ *
+ * @authors Inkscape authors, 2004-2019
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+enum SPAttributeEnum : unsigned;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+class SnapToolbar : public Toolbar {
+private:
+ bool _freeze;
+
+ // Toolbar widgets
+ Gtk::ToggleToolButton *_snap_global_item;
+ Gtk::ToggleToolButton *_snap_from_bbox_corner_item;
+ Gtk::ToggleToolButton *_snap_to_bbox_path_item;
+ Gtk::ToggleToolButton *_snap_to_bbox_node_item;
+ Gtk::ToggleToolButton *_snap_to_from_bbox_edge_midpoints_item;
+ Gtk::ToggleToolButton *_snap_to_from_bbox_edge_centers_item;
+ Gtk::ToggleToolButton *_snap_from_node_item;
+ Gtk::ToggleToolButton *_snap_to_item_path_item;
+ Gtk::ToggleToolButton *_snap_to_path_intersections_item;
+ Gtk::ToggleToolButton *_snap_to_item_node_item;
+ Gtk::ToggleToolButton *_snap_to_smooth_nodes_item;
+ Gtk::ToggleToolButton *_snap_to_from_line_midpoints_item;
+ Gtk::ToggleToolButton *_snap_from_others_item;
+ Gtk::ToggleToolButton *_snap_to_from_object_centers_item;
+ Gtk::ToggleToolButton *_snap_to_from_rotation_center_item;
+ Gtk::ToggleToolButton *_snap_to_from_text_baseline_item;
+ Gtk::ToggleToolButton *_snap_to_page_border_item;
+ Gtk::ToggleToolButton *_snap_to_grids_item;
+ Gtk::ToggleToolButton *_snap_to_guides_item;
+
+ void on_snap_toggled(SPAttributeEnum attr);
+
+protected:
+ SnapToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ static void update(SnapToolbar *tb);
+};
+
+}
+}
+}
+#endif /* !SEEN_SNAP_TOOLBAR_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/toolbar/spiral-toolbar.cpp b/src/ui/toolbar/spiral-toolbar.cpp
new file mode 100644
index 0000000..35981c1
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.cpp
@@ -0,0 +1,304 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Spiral aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spiral-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/separatortoolitem.h>
+#include <gtkmm/toolbutton.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "object/sp-spiral.h"
+
+#include "ui/icon-names.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "widgets/spinbutton-events.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector spiral_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::SpiralToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+SpiralToolbar::SpiralToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _freeze(false),
+ _repr(nullptr)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ {
+ _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")));
+ _mode_item->set_use_markup(true);
+ add(*_mode_item);
+ }
+
+ /* Revolution */
+ {
+ std::vector<Glib::ustring> labels = {_("just a curve"), "", _("one full revolution"), "", "", "", "", "", "", ""};
+ std::vector<double> values = { 0.01, 0.5, 1, 2, 3, 5, 10, 20, 50, 100};
+ auto revolution_val = prefs->getDouble("/tools/shapes/spiral/revolution", 3.0);
+ _revolution_adj = Gtk::Adjustment::create(revolution_val, 0.01, 1024.0, 0.1, 1.0);
+ _revolution_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-revolutions", _("Turns:"), _revolution_adj, 1, 2));
+ _revolution_item->set_tooltip_text(_("Number of revolutions"));
+ _revolution_item->set_custom_numeric_menu_data(values, labels);
+ _revolution_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _revolution_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed),
+ _revolution_adj, "revolution"));
+ add(*_revolution_item);
+ }
+
+ /* Expansion */
+ {
+ std::vector<Glib::ustring> labels = {_("circle"), _("edge is much denser"), _("edge is denser"), _("even"), _("center is denser"), _("center is much denser"), ""};
+ std::vector<double> values = { 0, 0.1, 0.5, 1, 1.5, 5, 20};
+ auto expansion_val = prefs->getDouble("/tools/shapes/spiral/expansion", 1.0);
+ _expansion_adj = Gtk::Adjustment::create(expansion_val, 0.0, 1000.0, 0.01, 1.0);
+
+ _expansion_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-expansion", _("Divergence:"), _expansion_adj));
+ _expansion_item->set_tooltip_text(_("How much denser/sparser are outer revolutions; 1 = uniform"));
+ _expansion_item->set_custom_numeric_menu_data(values, labels);
+ _expansion_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _expansion_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed),
+ _expansion_adj, "expansion"));
+ add(*_expansion_item);
+ }
+
+ /* T0 */
+ {
+ std::vector<Glib::ustring> labels = {_("starts from center"), _("starts mid-way"), _("starts near edge")};
+ std::vector<double> values = { 0, 0.5, 0.9};
+ auto t0_val = prefs->getDouble("/tools/shapes/spiral/t0", 0.0);
+ _t0_adj = Gtk::Adjustment::create(t0_val, 0.0, 0.999, 0.01, 1.0);
+ _t0_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-t0", _("Inner radius:"), _t0_adj));
+ _t0_item->set_tooltip_text(_("Radius of the innermost revolution (relative to the spiral size)"));
+ _t0_item->set_custom_numeric_menu_data(values, labels);
+ _t0_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _t0_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed),
+ _t0_adj, "t0"));
+ add(*_t0_item);
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Reset */
+ {
+ _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults")));
+ _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear"));
+ _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)"));
+ _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &SpiralToolbar::defaults));
+ add(*_reset_item);
+ }
+
+ _connection.reset(new sigc::connection(
+ desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &SpiralToolbar::selection_changed))));
+
+ show_all();
+}
+
+SpiralToolbar::~SpiralToolbar()
+{
+ if(_repr) {
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ if(_connection) {
+ _connection->disconnect();
+ }
+}
+
+GtkWidget *
+SpiralToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new SpiralToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+SpiralToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Glib::ustring const &value_name)
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/shapes/spiral/" + value_name,
+ adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ gchar* namespaced_name = g_strconcat("sodipodi:", value_name.data(), NULL);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (SP_IS_SPIRAL(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ sp_repr_set_svg_double( repr, namespaced_name,
+ adj->get_value() );
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ g_free(namespaced_name);
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SPIRAL,
+ _("Change spiral"));
+ }
+
+ _freeze = false;
+}
+
+void
+SpiralToolbar::defaults()
+{
+ // fixme: make settable
+ gdouble rev = 3;
+ gdouble exp = 1.0;
+ gdouble t0 = 0.0;
+
+ _revolution_adj->set_value(rev);
+ _expansion_adj->set_value(exp);
+ _t0_adj->set_value(t0);
+
+ if(_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(_desktop->canvas));
+}
+
+void
+SpiralToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+
+ if ( _repr ) {
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (SP_IS_SPIRAL(item)) {
+ n_selected++;
+ repr = item->getRepr();
+ }
+ }
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&spiral_tb_repr_events, this);
+ _repr->synthesizeEvents(&spiral_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ }
+}
+
+void
+SpiralToolbar::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const * /*name*/,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<SpiralToolbar *>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ double revolution = 3.0;
+ sp_repr_get_double(repr, "sodipodi:revolution", &revolution);
+ toolbar->_revolution_adj->set_value(revolution);
+
+ double expansion = 1.0;
+ sp_repr_get_double(repr, "sodipodi:expansion", &expansion);
+ toolbar->_expansion_adj->set_value(expansion);
+
+ double t0 = 0.0;
+ sp_repr_get_double(repr, "sodipodi:t0", &t0);
+ toolbar->_t0_adj->set_value(t0);
+
+ toolbar->_freeze = 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/toolbar/spiral-toolbar.h b/src/ui/toolbar/spiral-toolbar.h
new file mode 100644
index 0000000..9c27eb5
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SPIRAL_TOOLBAR_H
+#define SEEN_SPIRAL_TOOLBAR_H
+
+/**
+ * @file
+ * Spiral aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class SpiralToolbar : public Toolbar {
+private:
+ UI::Widget::LabelToolItem *_mode_item;
+
+ UI::Widget::SpinButtonToolItem *_revolution_item;
+ UI::Widget::SpinButtonToolItem *_expansion_item;
+ UI::Widget::SpinButtonToolItem *_t0_item;
+
+ Gtk::ToolButton *_reset_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _revolution_adj;
+ Glib::RefPtr<Gtk::Adjustment> _expansion_adj;
+ Glib::RefPtr<Gtk::Adjustment> _t0_adj;
+
+ bool _freeze;
+
+ XML::Node *_repr;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Glib::ustring const &value_name);
+ void defaults();
+ void selection_changed(Inkscape::Selection *selection);
+
+ std::unique_ptr<sigc::connection> _connection;
+
+protected:
+ SpiralToolbar(SPDesktop *desktop);
+ ~SpiralToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+};
+}
+}
+}
+
+#endif /* !SEEN_SPIRAL_TOOLBAR_H */
diff --git a/src/ui/toolbar/spray-toolbar.cpp b/src/ui/toolbar/spray-toolbar.cpp
new file mode 100644
index 0000000..9e7c4e8
--- /dev/null
+++ b/src/ui/toolbar/spray-toolbar.cpp
@@ -0,0 +1,550 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Spray aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2015 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spray-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "inkscape.h"
+
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+
+#include "ui/dialog/clonetiler.h"
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/panel-dialog.h"
+
+#include "ui/widget/spin-button-tool-item.h"
+
+// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object
+// with the mode: spray object in single path)
+// Please enable again when working on 1.0
+#define ENABLE_SPRAY_MODE_SINGLE_PATH
+
+Inkscape::UI::Dialog::CloneTiler *get_clone_tiler_panel(SPDesktop *desktop)
+{
+ if (Inkscape::UI::Dialog::PanelDialogBase *panel_dialog =
+ dynamic_cast<Inkscape::UI::Dialog::PanelDialogBase *>(desktop->_dlg_mgr->getDialog("CloneTiler"))) {
+ try {
+ Inkscape::UI::Dialog::CloneTiler &clone_tiler =
+ dynamic_cast<Inkscape::UI::Dialog::CloneTiler &>(panel_dialog->getPanel());
+ return &clone_tiler;
+ } catch (std::exception &e) { }
+ }
+
+ return nullptr;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+SprayToolbar::SprayToolbar(SPDesktop *desktop) :
+ Toolbar(desktop)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* Mode */
+ {
+ add_label(_("Mode:"));
+
+ Gtk::RadioToolButton::Group mode_group;
+
+ auto copy_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with copies")));
+ copy_mode_btn->set_tooltip_text(_("Spray copies of the initial selection"));
+ copy_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-copy"));
+ _mode_buttons.push_back(copy_mode_btn);
+
+ auto clone_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with clones")));
+ clone_mode_btn->set_tooltip_text(_("Spray clones of the initial selection"));
+ clone_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-clone"));
+ _mode_buttons.push_back(clone_mode_btn);
+
+#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH
+ auto union_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray single path")));
+ union_mode_btn->set_tooltip_text(_("Spray objects in a single path"));
+ union_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-union"));
+ _mode_buttons.push_back(union_mode_btn);
+#endif
+
+ auto eraser_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete sprayed items")));
+ eraser_mode_btn->set_tooltip_text(_("Delete sprayed items from selection"));
+ eraser_mode_btn->set_icon_name(INKSCAPE_ICON("draw-eraser"));
+ _mode_buttons.push_back(eraser_mode_btn);
+
+ int btn_idx = 0;
+ for (auto btn : _mode_buttons) {
+ btn->set_sensitive(true);
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::mode_changed), btn_idx++));
+ }
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Width */
+ std::vector<Glib::ustring> labels = {_("(narrow spray)"), "", "", "", _("(default)"), "", "", "", "", _("(broad spray)")};
+ std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100};
+ auto width_val = prefs->getDouble("/tools/spray/width", 15);
+ _width_adj = Gtk::Adjustment::create(width_val, 1, 100, 1.0, 10.0);
+ auto width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-width", _("Width:"), _width_adj, 1, 0));
+ width_item->set_tooltip_text(_("The width of the spray area (relative to the visible canvas area)"));
+ width_item->set_custom_numeric_menu_data(values, labels);
+ width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::width_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*width_item);
+ width_item->set_sensitive(true);
+ }
+
+ /* Use Pressure Width button */
+ {
+ auto pressure_item = add_toggle_button(_("Pressure"),
+ _("Use the pressure of the input device to alter the width of spray area"));
+ pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _usepressurewidth_pusher.reset(new UI::SimplePrefPusher(pressure_item, "/tools/spray/usepressurewidth"));
+ pressure_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ pressure_item,
+ "/tools/spray/usepressurewidth"));
+ }
+
+ { /* Population */
+ std::vector<Glib::ustring> labels = {_("(low population)"), "", "", "", _("(default)"), "", _("(high population)")};
+ std::vector<double> values = { 5, 20, 35, 50, 70, 85, 100};
+ auto population_val = prefs->getDouble("/tools/spray/population", 70);
+ _population_adj = Gtk::Adjustment::create(population_val, 1, 100, 1.0, 10.0);
+ _spray_population = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-population", _("Amount:"), _population_adj, 1, 0));
+ _spray_population->set_tooltip_text(_("Adjusts the number of items sprayed per click"));
+ _spray_population->set_custom_numeric_menu_data(values, labels);
+ _spray_population->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _population_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::population_value_changed));
+ //ege_adjustment_action_set_appearance( holder->_spray_population, TOOLBAR_SLIDER_HINT );
+ add(*_spray_population);
+ _spray_population->set_sensitive(true);
+ }
+
+ /* Use Pressure Population button */
+ {
+ auto pressure_population_item = add_toggle_button(_("Pressure"),
+ _("Use the pressure of the input device to alter the amount of sprayed objects"));
+ pressure_population_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _usepressurepopulation_pusher.reset(new UI::SimplePrefPusher(pressure_population_item, "/tools/spray/usepressurepopulation"));
+ pressure_population_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ pressure_population_item,
+ "/tools/spray/usepressurepopulation"));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ { /* Rotation */
+ std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high rotation variation)")};
+ std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100};
+ auto rotation_val = prefs->getDouble("/tools/spray/rotation_variation", 0);
+ _rotation_adj = Gtk::Adjustment::create(rotation_val, 0, 100, 1.0, 10.0);
+ _spray_rotation = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-rotation", _("Rotation:"), _rotation_adj, 1, 0));
+ // xgettext:no-c-format
+ _spray_rotation->set_tooltip_text(_("Variation of the rotation of the sprayed objects; 0% for the same rotation than the original object"));
+ _spray_rotation->set_custom_numeric_menu_data(values, labels);
+ _spray_rotation->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::rotation_value_changed));
+ // ege_adjustment_action_set_appearance(holder->_spray_rotation, TOOLBAR_SLIDER_HINT );
+ add(*_spray_rotation);
+ _spray_rotation->set_sensitive();
+ }
+
+ { /* Scale */
+ std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high scale variation)")};
+ std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100};
+ auto scale_val = prefs->getDouble("/tools/spray/scale_variation", 0);
+ _scale_adj = Gtk::Adjustment::create(scale_val, 0, 100, 1.0, 10.0);
+ _spray_scale = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-scale", C_("Spray tool", "Scale:"), _scale_adj, 1, 0));
+ // xgettext:no-c-format
+ _spray_scale->set_tooltip_text(_("Variation in the scale of the sprayed objects; 0% for the same scale than the original object"));
+ _spray_scale->set_custom_numeric_menu_data(values, labels);
+ _spray_scale->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::scale_value_changed));
+ // ege_adjustment_action_set_appearance( holder->_spray_scale, TOOLBAR_SLIDER_HINT );
+ add(*_spray_scale);
+ _spray_scale->set_sensitive(true);
+ }
+
+ /* Use Pressure Scale button */
+ {
+ _usepressurescale = add_toggle_button(_("Pressure"),
+ _("Use the pressure of the input device to alter the scale of new items"));
+ _usepressurescale->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _usepressurescale->set_active(prefs->getBool("/tools/spray/usepressurescale", false));
+ _usepressurescale->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_pressure_scale));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ /* Standard_deviation */
+ std::vector<Glib::ustring> labels = {_("(minimum scatter)"), "", "", "", "", "", _("(default)"), _("(maximum scatter)")};
+ std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100};
+ auto sd_val = prefs->getDouble("/tools/spray/standard_deviation", 70);
+ _sd_adj = Gtk::Adjustment::create(sd_val, 1, 100, 1.0, 10.0);
+ auto sd_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-standard-deviation", C_("Spray tool", "Scatter:"), _sd_adj, 1, 0));
+ sd_item->set_tooltip_text(_("Increase to scatter sprayed objects"));
+ sd_item->set_custom_numeric_menu_data(values, labels);
+ sd_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _sd_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::standard_deviation_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*sd_item);
+ sd_item->set_sensitive(true);
+ }
+
+ {
+ /* Mean */
+ std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(maximum mean)")};
+ std::vector<double> values = { 0, 5, 10, 20, 30, 50, 70, 100};
+ auto mean_val = prefs->getDouble("/tools/spray/mean", 0);
+ _mean_adj = Gtk::Adjustment::create(mean_val, 0, 100, 1.0, 10.0);
+ auto mean_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-mean", _("Focus:"), _mean_adj, 1, 0));
+ mean_item->set_tooltip_text(_("0 to spray a spot; increase to enlarge the ring radius"));
+ mean_item->set_custom_numeric_menu_data(values, labels);
+ mean_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _mean_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::mean_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*mean_item);
+ mean_item->set_sensitive(true);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Over No Transparent */
+ {
+ _over_no_transparent = add_toggle_button(_("Apply over no transparent areas"),
+ _("Apply over no transparent areas"));
+ _over_no_transparent->set_icon_name(INKSCAPE_ICON("object-visible"));
+ _over_no_transparent->set_active(prefs->getBool("/tools/spray/over_no_transparent", true));
+ _over_no_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _over_no_transparent,
+ "/tools/spray/over_no_transparent"));
+ }
+
+ /* Over Transparent */
+ {
+ _over_transparent = add_toggle_button(_("Apply over transparent areas"),
+ _("Apply over transparent areas"));
+ _over_transparent->set_icon_name(INKSCAPE_ICON("object-hidden"));
+ _over_transparent->set_active(prefs->getBool("/tools/spray/over_transparent", true));
+ _over_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _over_transparent,
+ "/tools/spray/over_transparent"));
+ }
+
+ /* Pick No Overlap */
+ {
+ _pick_no_overlap = add_toggle_button(_("No overlap between colors"),
+ _("No overlap between colors"));
+ _pick_no_overlap->set_icon_name(INKSCAPE_ICON("symbol-bigger"));
+ _pick_no_overlap->set_active(prefs->getBool("/tools/spray/pick_no_overlap", false));
+ _pick_no_overlap->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _pick_no_overlap,
+ "/tools/spray/pick_no_overlap"));
+ }
+
+ /* Overlap */
+ {
+ _no_overlap = add_toggle_button(_("Prevent overlapping objects"),
+ _("Prevent overlapping objects"));
+ _no_overlap->set_icon_name(INKSCAPE_ICON("distribute-randomize"));
+ _no_overlap->set_active(prefs->getBool("/tools/spray/no_overlap", false));
+ _no_overlap->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_no_overlap));
+ }
+
+ /* Offset */
+ {
+ std::vector<Glib::ustring> labels = {_("(minimum offset)"), "", "", "", _("(default)"), "", "", _("(maximum offset)")};
+ std::vector<double> values = { 0, 25, 50, 75, 100, 150, 200, 1000};
+ auto offset_val = prefs->getDouble("/tools/spray/offset", 100);
+ _offset_adj = Gtk::Adjustment::create(offset_val, 0, 1000, 1, 4);
+ _offset = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-offset", _("Offset %:"), _offset_adj, 0, 0));
+ _offset->set_tooltip_text(_("Increase to segregate objects more (value in percent)"));
+ _offset->set_custom_numeric_menu_data(values, labels);
+ _offset->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::offset_value_changed));
+ add(*_offset);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Picker */
+ {
+ _picker = add_toggle_button(_("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset."),
+ _("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset."));
+ _picker->set_icon_name(INKSCAPE_ICON("color-picker"));
+ _picker->set_active(prefs->getBool("/tools/spray/picker", false));
+ _picker->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_picker));
+ }
+
+ /* Pick Fill */
+ {
+ _pick_fill = add_toggle_button(_("Apply picked color to fill"),
+ _("Apply picked color to fill"));
+ _pick_fill->set_icon_name(INKSCAPE_ICON("paint-solid"));
+ _pick_fill->set_active(prefs->getBool("/tools/spray/pick_fill", false));
+ _pick_fill->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _pick_fill,
+ "/tools/spray/pick_fill"));
+ }
+
+ /* Pick Stroke */
+ {
+ _pick_stroke = add_toggle_button(_("Apply picked color to stroke"),
+ _("Apply picked color to stroke"));
+ _pick_stroke->set_icon_name(INKSCAPE_ICON("no-marker"));
+ _pick_stroke->set_active(prefs->getBool("/tools/spray/pick_stroke", false));
+ _pick_stroke->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _pick_stroke,
+ "/tools/spray/pick_stroke"));
+ }
+
+ /* Inverse Value Size */
+ {
+ _pick_inverse_value = add_toggle_button(_("Inverted pick value, retaining color in advanced trace mode"),
+ _("Inverted pick value, retaining color in advanced trace mode"));
+ _pick_inverse_value->set_icon_name(INKSCAPE_ICON("object-tweak-shrink"));
+ _pick_inverse_value->set_active(prefs->getBool("/tools/spray/pick_inverse_value", false));
+ _pick_inverse_value->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _pick_inverse_value,
+ "/tools/spray/pick_inverse_value"));
+ }
+
+ /* Pick from center */
+ {
+ _pick_center = add_toggle_button(_("Pick from center instead of average area."),
+ _("Pick from center instead of average area."));
+ _pick_center->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center"));
+ _pick_center->set_active(prefs->getBool("/tools/spray/pick_center", true));
+ _pick_center->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled),
+ _pick_center,
+ "/tools/spray/pick_center"));
+ }
+
+ gint mode = prefs->getInt("/tools/spray/mode", 1);
+ _mode_buttons[mode]->set_active();
+ show_all();
+ init();
+}
+
+GtkWidget *
+SprayToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new SprayToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+SprayToolbar::width_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/width",
+ _width_adj->get_value());
+}
+
+void
+SprayToolbar::mean_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/mean",
+ _mean_adj->get_value());
+}
+
+void
+SprayToolbar::standard_deviation_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/standard_deviation",
+ _sd_adj->get_value());
+}
+
+void
+SprayToolbar::mode_changed(int mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/spray/mode", mode);
+ init();
+}
+
+void
+SprayToolbar::init(){
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int mode = prefs->getInt("/tools/spray/mode", 0);
+
+ bool show = true;
+ if(mode == 3 || mode == 2){
+ show = false;
+ }
+ _no_overlap->set_visible(show);
+ _over_no_transparent->set_visible(show);
+ _over_transparent->set_visible(show);
+ _pick_no_overlap->set_visible(show);
+ _pick_stroke->set_visible(show);
+ _pick_fill->set_visible(show);
+ _pick_inverse_value->set_visible(show);
+ _pick_center->set_visible(show);
+ _picker->set_visible(show);
+ _offset->set_visible(show);
+ _pick_fill->set_visible(show);
+ _pick_stroke->set_visible(show);
+ _pick_inverse_value->set_visible(show);
+ _pick_center->set_visible(show);
+ if(mode == 2){
+ show = true;
+ }
+ _spray_rotation->set_visible(show);
+ update_widgets();
+}
+
+void
+SprayToolbar::population_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/population",
+ _population_adj->get_value());
+}
+
+void
+SprayToolbar::rotation_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/rotation_variation",
+ _rotation_adj->get_value());
+}
+
+void
+SprayToolbar::update_widgets()
+{
+ _offset_adj->set_value(100.0);
+
+ bool no_overlap_is_active = _no_overlap->get_active() && _no_overlap->get_visible();
+ _offset->set_visible(no_overlap_is_active);
+ if (_usepressurescale->get_active()) {
+ _scale_adj->set_value(0.0);
+ _spray_scale->set_sensitive(false);
+ } else {
+ _spray_scale->set_sensitive(true);
+ }
+
+ bool picker_is_active = _picker->get_active() && _picker->get_visible();
+ _pick_fill->set_visible(picker_is_active);
+ _pick_stroke->set_visible(picker_is_active);
+ _pick_inverse_value->set_visible(picker_is_active);
+ _pick_center->set_visible(picker_is_active);
+}
+
+void
+SprayToolbar::toggle_no_overlap()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _no_overlap->get_active();
+ prefs->setBool("/tools/spray/no_overlap", active);
+ update_widgets();
+}
+
+void
+SprayToolbar::scale_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/scale_variation",
+ _scale_adj->get_value());
+}
+
+void
+SprayToolbar::offset_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/spray/offset",
+ _offset_adj->get_value());
+}
+
+void
+SprayToolbar::toggle_pressure_scale()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _usepressurescale->get_active();
+ prefs->setBool("/tools/spray/usepressurescale", active);
+ if(active){
+ prefs->setDouble("/tools/spray/scale_variation", 0);
+ }
+ update_widgets();
+}
+
+void
+SprayToolbar::toggle_picker()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = _picker->get_active();
+ prefs->setBool("/tools/spray/picker", active);
+ if(active){
+ prefs->setBool("/dialogs/clonetiler/dotrace", false);
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (Inkscape::UI::Dialog::CloneTiler *ct = get_clone_tiler_panel(dt)){
+ dt->_dlg_mgr->showDialog("CloneTiler");
+ ct->show_page_trace();
+ }
+ }
+ update_widgets();
+}
+
+void
+SprayToolbar::on_pref_toggled(Gtk::ToggleToolButton *btn,
+ const Glib::ustring& path)
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool active = btn->get_active();
+ prefs->setBool(path, active);
+}
+
+void
+SprayToolbar::set_mode(int mode)
+{
+ _mode_buttons[mode]->set_active();
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/spray-toolbar.h b/src/ui/toolbar/spray-toolbar.h
new file mode 100644
index 0000000..4587cf0
--- /dev/null
+++ b/src/ui/toolbar/spray-toolbar.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SPRAY_TOOLBAR_H
+#define SEEN_SPRAY_TOOLBAR_H
+
+/**
+ * @file
+ * Spray aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2015 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class RadioToolButton;
+}
+
+namespace Inkscape {
+namespace UI {
+class SimplePrefPusher;
+
+namespace Widget {
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class SprayToolbar : public Toolbar {
+private:
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _mean_adj;
+ Glib::RefPtr<Gtk::Adjustment> _sd_adj;
+ Glib::RefPtr<Gtk::Adjustment> _population_adj;
+ Glib::RefPtr<Gtk::Adjustment> _rotation_adj;
+ Glib::RefPtr<Gtk::Adjustment> _offset_adj;
+ Glib::RefPtr<Gtk::Adjustment> _scale_adj;
+
+ std::unique_ptr<SimplePrefPusher> _usepressurewidth_pusher;
+ std::unique_ptr<SimplePrefPusher> _usepressurepopulation_pusher;
+
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+ UI::Widget::SpinButtonToolItem *_spray_population;
+ UI::Widget::SpinButtonToolItem *_spray_rotation;
+ UI::Widget::SpinButtonToolItem *_spray_scale;
+ Gtk::ToggleToolButton *_usepressurescale;
+ Gtk::ToggleToolButton *_picker;
+ Gtk::ToggleToolButton *_pick_center;
+ Gtk::ToggleToolButton *_pick_inverse_value;
+ Gtk::ToggleToolButton *_pick_fill;
+ Gtk::ToggleToolButton *_pick_stroke;
+ Gtk::ToggleToolButton *_pick_no_overlap;
+ Gtk::ToggleToolButton *_over_transparent;
+ Gtk::ToggleToolButton *_over_no_transparent;
+ Gtk::ToggleToolButton *_no_overlap;
+ UI::Widget::SpinButtonToolItem *_offset;
+
+ void width_value_changed();
+ void mean_value_changed();
+ void standard_deviation_value_changed();
+ void mode_changed(int mode);
+ void init();
+ void population_value_changed();
+ void rotation_value_changed();
+ void update_widgets();
+ void scale_value_changed();
+ void offset_value_changed();
+ void on_pref_toggled(Gtk::ToggleToolButton *btn,
+ const Glib::ustring& path);
+ void toggle_no_overlap();
+ void toggle_pressure_scale();
+ void toggle_picker();
+
+protected:
+ SprayToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ void set_mode(int mode);
+};
+}
+}
+}
+
+#endif /* !SEEN_SELECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/star-toolbar.cpp b/src/ui/toolbar/star-toolbar.cpp
new file mode 100644
index 0000000..2c020cf
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.cpp
@@ -0,0 +1,564 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Star aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "star-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "object/sp-star.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/star-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector star_tb_repr_events =
+{
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::StarToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+StarToolbar::StarToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))),
+ _repr(nullptr),
+ _freeze(false)
+{
+ _mode_item->set_use_markup(true);
+ add(*_mode_item);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false);
+
+ /* Flatsided checkbox */
+ {
+ Gtk::RadioToolButton::Group flat_item_group;
+
+ auto flat_polygon_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Polygon")));
+ flat_polygon_button->set_tooltip_text(_("Regular polygon (with one handle) instead of a star"));
+ flat_polygon_button->set_icon_name(INKSCAPE_ICON("draw-polygon"));
+ _flat_item_buttons.push_back(flat_polygon_button);
+
+ auto flat_star_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Star")));
+ flat_star_button->set_tooltip_text(_("Star instead of a regular polygon (with one handle)"));
+ flat_star_button->set_icon_name(INKSCAPE_ICON("draw-star"));
+ _flat_item_buttons.push_back(flat_star_button);
+
+ _flat_item_buttons[ isFlatSided ? 0 : 1 ]->set_active();
+
+ int btn_index = 0;
+
+ for (auto btn : _flat_item_buttons)
+ {
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &StarToolbar::side_mode_changed), btn_index++));
+ }
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Magnitude */
+ {
+ std::vector<Glib::ustring> labels = {_("triangle/tri-star"), _("square/quad-star"), _("pentagon/five-pointed star"), _("hexagon/six-pointed star"), "", "", "", "", ""};
+ std::vector<double> values = { 3, 4, 5, 6, 7, 8, 10, 12, 20};
+ auto magnitude_val = prefs->getDouble("/tools/shapes/star/magnitude", 3);
+ _magnitude_adj = Gtk::Adjustment::create(magnitude_val, 3, 1024, 1, 5);
+ _magnitude_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-magnitude", _("Corners:"), _magnitude_adj, 1.0, 0));
+ _magnitude_item->set_tooltip_text(_("Number of corners of a polygon or star"));
+ _magnitude_item->set_custom_numeric_menu_data(values, labels);
+ _magnitude_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _magnitude_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::magnitude_value_changed));
+ _magnitude_item->set_sensitive(true);
+ add(*_magnitude_item);
+ }
+
+ /* Spoke ratio */
+ {
+ std::vector<Glib::ustring> labels = {_("thin-ray star"), "", _("pentagram"), _("hexagram"), _("heptagram"), _("octagram"), _("regular polygon")};
+ std::vector<double> values = { 0.01, 0.2, 0.382, 0.577, 0.692, 0.765, 1};
+ auto prop_val = prefs->getDouble("/tools/shapes/star/proportion", 0.5);
+ _spoke_adj = Gtk::Adjustment::create(prop_val, 0.01, 1.0, 0.01, 0.1);
+ _spoke_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-spoke", _("Spoke ratio:"), _spoke_adj));
+ // TRANSLATORS: Tip radius of a star is the distance from the center to the farthest handle.
+ // Base radius is the same for the closest handle.
+ _spoke_item->set_tooltip_text(_("Base radius to tip radius ratio"));
+ _spoke_item->set_custom_numeric_menu_data(values, labels);
+ _spoke_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _spoke_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::proportion_value_changed));
+
+ add(*_spoke_item);
+ }
+
+ /* Roundedness */
+ {
+ std::vector<Glib::ustring> labels = {_("stretched"), _("twisted"), _("slightly pinched"), _("NOT rounded"), _("slightly rounded"),
+ _("visibly rounded"), _("well rounded"), _("amply rounded"), "", _("stretched"), _("blown up")};
+ std::vector<double> values = {-1, -0.2, -0.03, 0, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 10};
+ auto roundedness_val = prefs->getDouble("/tools/shapes/star/rounded", 0.0);
+ _roundedness_adj = Gtk::Adjustment::create(roundedness_val, -10.0, 10.0, 0.01, 0.1);
+ _roundedness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-roundedness", _("Rounded:"), _roundedness_adj));
+ _roundedness_item->set_tooltip_text(_("How rounded are the corners (0 for sharp)"));
+ _roundedness_item->set_custom_numeric_menu_data(values, labels);
+ _roundedness_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _roundedness_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::rounded_value_changed));
+ _roundedness_item->set_sensitive(true);
+ add(*_roundedness_item);
+ }
+
+ /* Randomization */
+ {
+ std::vector<Glib::ustring> labels = {_("NOT randomized"), _("slightly irregular"), _("visibly randomized"), _("strongly randomized"), _("blown up")};
+ std::vector<double> values = { 0, 0.01, 0.1, 0.5, 10};
+ auto randomized_val = prefs->getDouble("/tools/shapes/star/randomized", 0.0);
+ _randomization_adj = Gtk::Adjustment::create(randomized_val, -10.0, 10.0, 0.001, 0.01);
+ _randomization_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-randomized", _("Randomized:"), _randomization_adj, 0.1, 3));
+ _randomization_item->set_tooltip_text(_("Scatter randomly the corners and angles"));
+ _randomization_item->set_custom_numeric_menu_data(values, labels);
+ _randomization_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _randomization_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::randomized_value_changed));
+ _randomization_item->set_sensitive(true);
+ add(*_randomization_item);
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Reset */
+ {
+ _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults")));
+ _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear"));
+ _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)"));
+ _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &StarToolbar::defaults));
+ _reset_item->set_sensitive(true);
+ add(*_reset_item);
+ }
+
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &StarToolbar::watch_ec));
+
+ show_all();
+ _spoke_item->set_visible(!isFlatSided);
+}
+
+StarToolbar::~StarToolbar()
+{
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+GtkWidget *
+StarToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new StarToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+StarToolbar::side_mode_changed(int mode)
+{
+ bool flat = (mode == 0);
+
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool( "/tools/shapes/star/isflatsided", flat );
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ bool modmade = false;
+
+ if (_spoke_item) {
+ _spoke_item->set_visible(!flat);
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttribute("inkscape:flatsided", flat ? "true" : "false" );
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ flat ? _("Make polygon") : _("Make star"));
+ }
+
+ _freeze = false;
+}
+
+void
+StarToolbar::magnitude_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ // do not remember prefs if this call is initiated by an undo change, because undoing object
+ // creation sets bogus values to its attributes before it is deleted
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/shapes/star/magnitude",
+ (gint)_magnitude_adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ sp_repr_set_int(repr,"sodipodi:sides",
+ (gint)_magnitude_adj->get_value());
+ double arg1 = 0.5;
+ sp_repr_get_double(repr, "sodipodi:arg1", &arg1);
+ sp_repr_set_svg_double(repr, "sodipodi:arg2",
+ (arg1 + M_PI / (gint)_magnitude_adj->get_value()));
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ _("Star: Change number of corners"));
+ }
+
+ _freeze = false;
+}
+
+void
+StarToolbar::proportion_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ if (!std::isnan(_spoke_adj->get_value())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/shapes/star/proportion",
+ _spoke_adj->get_value());
+ }
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+
+ gdouble r1 = 1.0;
+ gdouble r2 = 1.0;
+ sp_repr_get_double(repr, "sodipodi:r1", &r1);
+ sp_repr_get_double(repr, "sodipodi:r2", &r2);
+ if (r2 < r1) {
+ sp_repr_set_svg_double(repr, "sodipodi:r2",
+ r1*_spoke_adj->get_value());
+ } else {
+ sp_repr_set_svg_double(repr, "sodipodi:r1",
+ r2*_spoke_adj->get_value());
+ }
+
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ _("Star: Change spoke ratio"));
+ }
+
+ _freeze = false;
+}
+
+void
+StarToolbar::rounded_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/shapes/star/rounded", (gdouble) _roundedness_adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ sp_repr_set_svg_double(repr, "inkscape:rounded",
+ (gdouble) _roundedness_adj->get_value());
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ _("Star: Change rounding"));
+ }
+
+ _freeze = false;
+}
+
+void
+StarToolbar::randomized_value_changed()
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/shapes/star/randomized",
+ (gdouble) _randomization_adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ sp_repr_set_svg_double(repr, "inkscape:randomized",
+ (gdouble) _randomization_adj->get_value());
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ _("Star: Change randomization"));
+ }
+
+ _freeze = false;
+}
+
+void
+StarToolbar::defaults()
+{
+
+ // FIXME: in this and all other _default functions, set some flag telling the value_changed
+ // callbacks to lump all the changes for all selected objects in one undo step
+
+ // fixme: make settable in prefs!
+ gint mag = 5;
+ gdouble prop = 0.5;
+ gboolean flat = FALSE;
+ gdouble randomized = 0;
+ gdouble rounded = 0;
+
+ _flat_item_buttons[ flat ? 0 : 1 ]->set_active();
+
+ _spoke_item->set_visible(!flat);
+
+ _magnitude_adj->set_value(mag);
+ _spoke_adj->set_value(prop);
+ _roundedness_adj->set_value(rounded);
+ _randomization_adj->set_value(randomized);
+}
+
+void
+StarToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (dynamic_cast<Inkscape::UI::Tools::StarTool const*>(ec) != nullptr) {
+ _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &StarToolbar::selection_changed));
+ selection_changed(desktop->getSelection());
+ } else {
+ if (_changed)
+ _changed.disconnect();
+ }
+}
+
+/**
+ * \param selection Should not be NULL.
+ */
+void
+StarToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ n_selected++;
+ repr = item->getRepr();
+ }
+ }
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&star_tb_repr_events, this);
+ _repr->synthesizeEvents(&star_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected stars
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Change:</b>"));
+ }
+}
+
+void
+StarToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer dataPointer)
+{
+ auto toolbar = reinterpret_cast<StarToolbar *>(dataPointer);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false);
+
+ if (!strcmp(name, "inkscape:randomized")) {
+ double randomized = 0.0;
+ sp_repr_get_double(repr, "inkscape:randomized", &randomized);
+ toolbar->_randomization_adj->set_value(randomized);
+ } else if (!strcmp(name, "inkscape:rounded")) {
+ double rounded = 0.0;
+ sp_repr_get_double(repr, "inkscape:rounded", &rounded);
+ toolbar->_roundedness_adj->set_value(rounded);
+ } else if (!strcmp(name, "inkscape:flatsided")) {
+ char const *flatsides = repr->attribute("inkscape:flatsided");
+ if ( flatsides && !strcmp(flatsides,"false") ) {
+ toolbar->_flat_item_buttons[1]->set_active();
+ toolbar->_spoke_item->set_visible(true);
+ } else {
+ toolbar->_flat_item_buttons[0]->set_active();
+ toolbar->_spoke_item->set_visible(false);
+ }
+ } else if ((!strcmp(name, "sodipodi:r1") || !strcmp(name, "sodipodi:r2")) && (!isFlatSided) ) {
+ gdouble r1 = 1.0;
+ gdouble r2 = 1.0;
+ sp_repr_get_double(repr, "sodipodi:r1", &r1);
+ sp_repr_get_double(repr, "sodipodi:r2", &r2);
+ if (r2 < r1) {
+ toolbar->_spoke_adj->set_value(r2/r1);
+ } else {
+ toolbar->_spoke_adj->set_value(r1/r2);
+ }
+ } else if (!strcmp(name, "sodipodi:sides")) {
+ int sides = 0;
+ sp_repr_get_int(repr, "sodipodi:sides", &sides);
+ toolbar->_magnitude_adj->set_value(sides);
+ }
+
+ toolbar->_freeze = 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 :
diff --git a/src/ui/toolbar/star-toolbar.h b/src/ui/toolbar/star-toolbar.h
new file mode 100644
index 0000000..c44caab
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_STAR_TOOLBAR_H
+#define SEEN_STAR_TOOLBAR_H
+
+/**
+ * @file
+ * Star aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class RadioToolButton;
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class StarToolbar : public Toolbar {
+private:
+ UI::Widget::LabelToolItem *_mode_item;
+ std::vector<Gtk::RadioToolButton *> _flat_item_buttons;
+ UI::Widget::SpinButtonToolItem *_magnitude_item;
+ UI::Widget::SpinButtonToolItem *_spoke_item;
+ UI::Widget::SpinButtonToolItem *_roundedness_item;
+ UI::Widget::SpinButtonToolItem *_randomization_item;
+ Gtk::ToolButton *_reset_item;
+
+ XML::Node *_repr;
+
+ Glib::RefPtr<Gtk::Adjustment> _magnitude_adj;
+ Glib::RefPtr<Gtk::Adjustment> _spoke_adj;
+ Glib::RefPtr<Gtk::Adjustment> _roundedness_adj;
+ Glib::RefPtr<Gtk::Adjustment> _randomization_adj;
+
+ bool _freeze;
+ sigc::connection _changed;
+
+ void side_mode_changed(int mode);
+ void magnitude_value_changed();
+ void proportion_value_changed();
+ void rounded_value_changed();
+ void randomized_value_changed();
+ void defaults();
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+protected:
+ StarToolbar(SPDesktop *desktop);
+ ~StarToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer dataPointer);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_SELECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/text-toolbar.cpp b/src/ui/toolbar/text-toolbar.cpp
new file mode 100644
index 0000000..3b9a152
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.cpp
@@ -0,0 +1,2540 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Text aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 1999-2013 authors
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include "text-toolbar.h"
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "selection-chemistry.h"
+#include "verbs.h"
+
+#include "libnrtype/font-lister.h"
+
+#include "display/sp-canvas.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-root.h"
+#include "object/sp-text.h"
+#include "object/sp-tspan.h"
+#include "object/sp-string.h"
+
+#include "svg/css-ostringstream.h"
+#include "ui/icon-names.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/text-tool.h"
+#include "ui/widget/combo-box-entry-tool-item.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+#include "util/units.h"
+
+#include "widgets/style-utils.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+using Inkscape::UI::Widget::UnitTracker;
+
+//#define DEBUG_TEXT
+
+//########################
+//## Text Toolbox ##
+//########################
+
+// Functions for debugging:
+#ifdef DEBUG_TEXT
+static void sp_print_font(SPStyle *query)
+{
+
+
+ bool family_set = query->font_family.set;
+ bool style_set = query->font_style.set;
+ bool fontspec_set = query->font_specification.set;
+
+ std::cout << " Family set? " << family_set
+ << " Style set? " << style_set
+ << " FontSpec set? " << fontspec_set
+ << std::endl;
+}
+
+static void sp_print_fontweight( SPStyle *query ) {
+ const gchar* names[] = {"100", "200", "300", "400", "500", "600", "700", "800", "900",
+ "NORMAL", "BOLD", "LIGHTER", "BOLDER", "Out of range"};
+ // Missing book = 380
+ int index = query->font_weight.computed;
+ if (index < 0 || index > 13)
+ index = 13;
+ std::cout << " Weight: " << names[ index ]
+ << " (" << query->font_weight.computed << ")" << std::endl;
+}
+
+static void sp_print_fontstyle( SPStyle *query ) {
+
+ const gchar* names[] = {"NORMAL", "ITALIC", "OBLIQUE", "Out of range"};
+ int index = query->font_style.computed;
+ if( index < 0 || index > 3 ) index = 3;
+ std::cout << " Style: " << names[ index ] << std::endl;
+
+}
+#endif
+
+static bool is_relative( Unit const *unit ) {
+ return (unit->abbr == "" || unit->abbr == "em" || unit->abbr == "ex" || unit->abbr == "%");
+}
+
+static bool is_relative(SPCSSUnit const unit)
+{
+ return (unit == SP_CSS_UNIT_NONE || unit == SP_CSS_UNIT_EM || unit == SP_CSS_UNIT_EX ||
+ unit == SP_CSS_UNIT_PERCENT);
+}
+
+// Set property for object, but unset all descendents
+// Should probably be moved to desktop_style.cpp
+static void recursively_set_properties(SPObject *object, SPCSSAttr *css, bool unset_descendents = true)
+{
+ object->changeCSS (css, "style");
+
+ SPCSSAttr *css_unset = sp_repr_css_attr_unset_all( css );
+ std::vector<SPObject *> children = object->childList(false);
+ for (auto i: children) {
+ recursively_set_properties(i, unset_descendents ? css_unset : css);
+ }
+ sp_repr_css_attr_unref (css_unset);
+}
+
+/*
+ * Set the default list of font sizes, scaled to the users preferred unit
+ */
+static void sp_text_set_sizes(GtkListStore* model_size, int unit)
+{
+ gtk_list_store_clear(model_size);
+
+ // List of font sizes for dropchange-down menu
+ int sizes[] = {
+ 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
+ 32, 36, 40, 48, 56, 64, 72, 144
+ };
+
+ // Array must be same length as SPCSSUnit in style.h
+ float ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16};
+
+ for(int i : sizes) {
+ GtkTreeIter iter;
+ Glib::ustring size = Glib::ustring::format(i / (float)ratios[unit]);
+ gtk_list_store_append( model_size, &iter );
+ gtk_list_store_set( model_size, &iter, 0, size.c_str(), -1 );
+ }
+}
+
+
+// TODO: possibly share with font-selector by moving most code to font-lister (passing family name)
+static void sp_text_toolbox_select_cb( GtkEntry* entry, GtkEntryIconPosition /*position*/, GdkEvent /*event*/, gpointer /*data*/ ) {
+
+ Glib::ustring family = gtk_entry_get_text ( entry );
+ //std::cout << "text_toolbox_missing_font_cb: selecting: " << family << std::endl;
+
+ // Get all items with matching font-family set (not inherited!).
+ std::vector<SPItem*> selectList;
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *document = desktop->getDocument();
+ std::vector<SPItem*> x,y;
+ std::vector<SPItem*> allList = get_all_items(x, document->getRoot(), desktop, false, false, true, y);
+ for(std::vector<SPItem*>::const_reverse_iterator i=allList.rbegin();i!=allList.rend(); ++i){
+ SPItem *item = *i;
+ SPStyle *style = item->style;
+
+ if (style) {
+
+ Glib::ustring family_style;
+ if (style->font_family.set) {
+ family_style = style->font_family.value();
+ //std::cout << " family style from font_family: " << family_style << std::endl;
+ }
+ else if (style->font_specification.set) {
+ family_style = style->font_specification.value();
+ //std::cout << " family style from font_spec: " << family_style << std::endl;
+ }
+
+ if (family_style.compare( family ) == 0 ) {
+ //std::cout << " found: " << item->getId() << std::endl;
+ selectList.push_back(item);
+ }
+ }
+ }
+
+ // Update selection
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->clear();
+ //std::cout << " list length: " << g_slist_length ( selectList ) << std::endl;
+ selection->setList(selectList);
+}
+
+static void text_toolbox_watch_ec(SPDesktop* dt, Inkscape::UI::Tools::ToolBase* ec, GObject* holder);
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+TextToolbar::TextToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+ , _freeze(false)
+ , _text_style_from_prefs(false)
+ , _outer(true)
+ , _updating(false)
+ , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _tracker_fs(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _cusor_numbers(0)
+{
+ /* Line height unit tracker */
+ _tracker->prependUnit(unit_table.getUnit("")); // Ratio
+ _tracker->addUnit(unit_table.getUnit("%"));
+ _tracker->addUnit(unit_table.getUnit("em"));
+ _tracker->addUnit(unit_table.getUnit("ex"));
+ _tracker->setActiveUnit(unit_table.getUnit(""));
+ // We change only the display value
+ _tracker->changeLabel("lines", 0, true);
+ _tracker_fs->setActiveUnit(unit_table.getUnit("mm"));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* Font family */
+ {
+ // Font list
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+ fontlister->update_font_list( SP_ACTIVE_DESKTOP->getDocument());
+ Glib::RefPtr<Gtk::ListStore> store = fontlister->get_font_list();
+ GtkListStore* model = store->gobj();
+
+ _font_family_item =
+ Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontFamilyAction",
+ _("Font Family"),
+ _("Select Font Family (Alt-X to access)"),
+ GTK_TREE_MODEL(model),
+ -1, // Entry width
+ 50, // Extra list width
+ (gpointer)font_lister_cell_data_func2, // Cell layout
+ (gpointer)font_lister_separator_func2,
+ GTK_WIDGET(desktop->canvas))); // Focus widget
+ _font_family_item->popup_enable(); // Enable entry completion
+ gchar *const info = _("Select all text with this font-family");
+ _font_family_item->set_info( info ); // Show selection icon
+ _font_family_item->set_info_cb( (gpointer)sp_text_toolbox_select_cb );
+
+ gchar *const warning = _("Font not found on system");
+ _font_family_item->set_warning( warning ); // Show icon w/ tooltip if font missing
+ _font_family_item->set_warning_cb( (gpointer)sp_text_toolbox_select_cb );
+
+ //ink_comboboxentry_action_set_warning_callback( act, sp_text_fontfamily_select_all );
+ _font_family_item->set_altx_name( "altx-text" ); // Set Alt-X keyboard shortcut
+ _font_family_item->signal_changed().connect( sigc::mem_fun(*this, &TextToolbar::fontfamily_value_changed) );
+ add(*_font_family_item);
+
+ // Change style of drop-down from menu to list
+ auto css_provider = gtk_css_provider_new();
+ gtk_css_provider_load_from_data(css_provider,
+ "#TextFontFamilyAction_combobox {\n"
+ " -GtkComboBox-appears-as-list: true;\n"
+ "}\n",
+ -1, nullptr);
+
+ auto screen = gdk_screen_get_default();
+ _font_family_item->focus_on_click(false);
+ gtk_style_context_add_provider_for_screen(screen,
+ GTK_STYLE_PROVIDER(css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ }
+
+ /* Font styles */
+ {
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::ListStore> store = fontlister->get_style_list();
+ GtkListStore* model_style = store->gobj();
+
+ _font_style_item = Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontStyleAction",
+ _("Font Style"),
+ _("Font style"),
+ GTK_TREE_MODEL(model_style),
+ 12, // Width in characters
+ 0, // Extra list width
+ nullptr, // Cell layout
+ nullptr, // Separator
+ GTK_WIDGET(desktop->canvas))); // Focus widget
+
+ _font_style_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontstyle_value_changed));
+ _font_style_item->focus_on_click(false);
+ add(*_font_style_item);
+ }
+
+ add_separator();
+
+ /* Font size */
+ {
+ // List of font sizes for drop-down menu
+ GtkListStore* model_size = gtk_list_store_new( 1, G_TYPE_STRING );
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+
+ sp_text_set_sizes(model_size, unit);
+
+ auto unit_str = sp_style_get_css_unit_string(unit);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")");
+
+ _font_size_item = Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontSizeAction",
+ _("Font Size"),
+ tooltip,
+ GTK_TREE_MODEL(model_size),
+ 8, // Width in characters
+ 0, // Extra list width
+ nullptr, // Cell layout
+ nullptr, // Separator
+ GTK_WIDGET(desktop->canvas))); // Focus widget
+
+ _font_size_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontsize_value_changed));
+ _font_size_item->focus_on_click(false);
+ add(*_font_size_item);
+ }
+ /* Font_ size units */
+ {
+ _font_size_units_item = _tracker_fs->create_tool_item(_("Units"), (""));
+ _font_size_units_item->signal_changed_after().connect(
+ sigc::mem_fun(*this, &TextToolbar::fontsize_unit_changed));
+ _font_size_units_item->focus_on_click(false);
+ add(*_font_size_units_item);
+ }
+ {
+ // Drop down menu
+ std::vector<Glib::ustring> labels = {_("Smaller spacing"), "", "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", _("Larger spacing")};
+ std::vector<double> values = { 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0};
+
+ auto line_height_val = 1.25;
+ _line_height_adj = Gtk::Adjustment::create(line_height_val, 0.0, 1000.0, 0.1, 1.0);
+ _line_height_item =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("text-line-height", "", _line_height_adj, 0.1, 2));
+ _line_height_item->set_tooltip_text(_("Spacing between baselines"));
+ _line_height_item->set_custom_numeric_menu_data(values, labels);
+ _line_height_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _line_height_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_value_changed));
+ //_tracker->addAdjustment(_line_height_adj->gobj()); // (Alex V) Why is this commented out?
+ _line_height_item->set_sensitive(true);
+ _line_height_item->set_icon(INKSCAPE_ICON("text_line_spacing"));
+ add(*_line_height_item);
+ }
+ /* Line height units */
+ {
+ _line_height_units_item = _tracker->create_tool_item( _("Units"), (""));
+ _line_height_units_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_unit_changed));
+ _line_height_units_item->focus_on_click(false);
+ add(*_line_height_units_item);
+ }
+
+ Gtk::SeparatorToolItem *separator = Gtk::manage(new Gtk::SeparatorToolItem());
+ /* Alignment */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Align left");
+ row[columns.col_tooltip ] = _("Align left");
+ row[columns.col_icon ] = INKSCAPE_ICON("format-justify-left");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Align center");
+ row[columns.col_tooltip ] = _("Align center");
+ row[columns.col_icon ] = INKSCAPE_ICON("format-justify-center");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Align right");
+ row[columns.col_tooltip ] = _("Align right");
+ row[columns.col_icon ] = INKSCAPE_ICON("format-justify-right");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Justify");
+ row[columns.col_tooltip ] = _("Justify (only flowed text)");
+ row[columns.col_icon ] = INKSCAPE_ICON("format-justify-fill");
+ row[columns.col_sensitive] = false;
+
+ _align_item =
+ UI::Widget::ComboToolItem::create(_("Alignment"), // Label
+ _("Text alignment"), // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+ _align_item->use_icon( true );
+ _align_item->use_label( false );
+ gint mode = prefs->getInt("/tools/text/align_mode", 0);
+ _align_item->set_active( mode );
+
+ add(*_align_item);
+ _align_item->focus_on_click(false);
+ _align_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::align_mode_changed));
+ }
+
+ /* Style - Superscript */
+ {
+ _superscript_item = Gtk::manage(new Gtk::ToggleToolButton());
+ _superscript_item->set_label(_("Toggle superscript"));
+ _superscript_item->set_tooltip_text(_("Toggle superscript"));
+ _superscript_item->set_icon_name(INKSCAPE_ICON("text_superscript"));
+ _superscript_item->set_name("text-superscript");
+ add(*_superscript_item);
+ _superscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _superscript_item));
+ _superscript_item->set_active(prefs->getBool("/tools/text/super", false));
+ }
+
+ /* Style - Subscript */
+ {
+ _subscript_item = Gtk::manage(new Gtk::ToggleToolButton());
+ _subscript_item->set_label(_("Toggle subscript"));
+ _subscript_item->set_tooltip_text(_("Toggle subscript"));
+ _subscript_item->set_icon_name(INKSCAPE_ICON("text_subscript"));
+ _subscript_item->set_name("text-subscript");
+ add(*_subscript_item);
+ _subscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _subscript_item));
+ _subscript_item->set_active(prefs->getBool("/tools/text/sub", false));
+ }
+
+ /* Letter spacing */
+ {
+ // Drop down menu
+ std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")};
+ std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0};
+ auto letter_spacing_val = prefs->getDouble("/tools/text/letterspacing", 0.0);
+ _letter_spacing_adj = Gtk::Adjustment::create(letter_spacing_val, -100.0, 100.0, 0.01, 0.10);
+ _letter_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-letter-spacing", _("Letter:"), _letter_spacing_adj, 0.1, 2));
+ _letter_spacing_item->set_tooltip_text(_("Spacing between letters (px)"));
+ _letter_spacing_item->set_custom_numeric_menu_data(values, labels);
+ _letter_spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _letter_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::letterspacing_value_changed));
+ add(*_letter_spacing_item);
+
+ _letter_spacing_item->set_sensitive(true);
+ _letter_spacing_item->set_icon(INKSCAPE_ICON("text_letter_spacing"));
+ }
+
+ /* Word spacing */
+ {
+ // Drop down menu
+ std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")};
+ std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0};
+ auto word_spacing_val = prefs->getDouble("/tools/text/wordspacing", 0.0);
+ _word_spacing_adj = Gtk::Adjustment::create(word_spacing_val, -100.0, 100.0, 0.01, 0.10);
+ _word_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-word-spacing", _("Word:"), _word_spacing_adj, 0.1, 2));
+ _word_spacing_item->set_tooltip_text(_("Spacing between words (px)"));
+ _word_spacing_item->set_custom_numeric_menu_data(values, labels);
+ _word_spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _word_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::wordspacing_value_changed));
+
+ add(*_word_spacing_item);
+ _word_spacing_item->set_sensitive(true);
+ _word_spacing_item->set_icon(INKSCAPE_ICON("text_word_spacing"));
+ }
+
+ /* Character kerning (horizontal shift) */
+ {
+ // Drop down menu
+ std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 };
+ auto dx_val = prefs->getDouble("/tools/text/dx", 0.0);
+ _dx_adj = Gtk::Adjustment::create(dx_val, -100.0, 100.0, 0.01, 0.1);
+ _dx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dx", _("Kern:"), _dx_adj, 0.1, 2));
+ _dx_item->set_custom_numeric_menu_data(values);
+ _dx_item->set_tooltip_text(_("Horizontal kerning (px)"));
+ _dx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _dx_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dx_value_changed));
+ add(*_dx_item);
+ _dx_item->set_sensitive(true);
+ _dx_item->set_icon(INKSCAPE_ICON("text_horz_kern"));
+ }
+
+ /* Character vertical shift */
+ {
+ // Drop down menu
+ std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 };
+ auto dy_val = prefs->getDouble("/tools/text/dy", 0.0);
+ _dy_adj = Gtk::Adjustment::create(dy_val, -100.0, 100.0, 0.01, 0.1);
+ _dy_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dy", _("Vert:"), _dy_adj, 0.1, 2));
+ _dy_item->set_tooltip_text(_("Vertical kerning (px)"));
+ _dy_item->set_custom_numeric_menu_data(values);
+ _dy_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _dy_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dy_value_changed));
+ _dy_item->set_sensitive(true);
+ _dy_item->set_icon(INKSCAPE_ICON("text_vert_kern"));
+ add(*_dy_item);
+ }
+
+ /* Character rotation */
+ {
+ std::vector<double> values = { -90, -45, -30, -15, 0, 15, 30, 45, 90, 180 };
+ auto rotation_val = prefs->getDouble("/tools/text/rotation", 0.0);
+ _rotation_adj = Gtk::Adjustment::create(rotation_val, -180.0, 180.0, 0.1, 1.0);
+ _rotation_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-rotation", _("Rot:"), _rotation_adj, 0.1, 2));
+ _rotation_item->set_tooltip_text(_("Character rotation (degrees)"));
+ _rotation_item->set_custom_numeric_menu_data(values);
+ _rotation_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::rotation_value_changed));
+ _rotation_item->set_sensitive();
+ _rotation_item->set_icon(INKSCAPE_ICON("text_rotation"));
+ add(*_rotation_item);
+ }
+
+
+ /* Writing mode (Horizontal, Vertical-LR, Vertical-RL) */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Horizontal");
+ row[columns.col_tooltip ] = _("Horizontal text");
+ row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Vertical — RL");
+ row[columns.col_tooltip ] = _("Vertical text — lines: right to left");
+ row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Vertical — LR");
+ row[columns.col_tooltip ] = _("Vertical text — lines: left to right");
+ row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical-lr");
+ row[columns.col_sensitive] = true;
+
+ _writing_mode_item =
+ UI::Widget::ComboToolItem::create( _("Writing mode"), // Label
+ _("Block progression"), // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+ _writing_mode_item->use_icon(true);
+ _writing_mode_item->use_label( false );
+ gint mode = prefs->getInt("/tools/text/writing_mode", 0);
+ _writing_mode_item->set_active( mode );
+ add(*_writing_mode_item);
+ _writing_mode_item->focus_on_click(false);
+ _writing_mode_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::writing_mode_changed));
+ }
+
+
+ /* Text (glyph) orientation (Auto (mixed), Upright, Sideways) */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Auto");
+ row[columns.col_tooltip ] = _("Auto glyph orientation");
+ row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-auto");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Upright");
+ row[columns.col_tooltip ] = _("Upright glyph orientation");
+ row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-upright");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Sideways");
+ row[columns.col_tooltip ] = _("Sideways glyph orientation");
+ row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-sideways");
+ row[columns.col_sensitive] = true;
+
+ _orientation_item =
+ UI::Widget::ComboToolItem::create(_("Text orientation"), // Label
+ _("Text (glyph) orientation in vertical text."), // Tooltip
+ "Not Used", // Icon
+ store ); // List store
+ _orientation_item->use_icon(true);
+ _orientation_item->use_label(false);
+ gint mode = prefs->getInt("/tools/text/text_orientation", 0);
+ _orientation_item->set_active( mode );
+ _orientation_item->focus_on_click(false);
+ add(*_orientation_item);
+
+ _orientation_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::orientation_changed));
+ }
+
+ // Text direction (predominant direction of horizontal text).
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("LTR");
+ row[columns.col_tooltip ] = _("Left to right text");
+ row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal");
+ row[columns.col_sensitive] = true;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("RTL");
+ row[columns.col_tooltip ] = _("Right to left text");
+ row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-r2l");
+ row[columns.col_sensitive] = true;
+
+ _direction_item =
+ UI::Widget::ComboToolItem::create( _("Text direction"), // Label
+ _("Text direction for normally horizontal text."), // Tooltip
+ "Not Used", // Icon
+ store ); // List store
+ _direction_item->use_icon(true);
+ _direction_item->use_label(false);
+ gint mode = prefs->getInt("/tools/text/text_direction", 0);
+ _direction_item->set_active( mode );
+ _direction_item->focus_on_click(false);
+ add(*_direction_item);
+
+ _direction_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::direction_changed));
+ }
+
+ show_all();
+
+ // we emit a selection change on tool switch to text
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &TextToolbar::watch_ec));
+}
+
+/*
+ * Set the style, depending on the inner or outer text being selected
+ */
+void TextToolbar::text_outer_set_style(SPCSSAttr *css)
+{
+ // Calling sp_desktop_set_style will result in a call to TextTool::_styleSet() which
+ // will set the style on selected text inside the <text> element. If we want to set
+ // the style on the outer <text> objects we need to bypass this call.
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(_outer) {
+ // Apply css to parent text objects directly.
+ for (auto i : desktop->getSelection()->items()) {
+ SPItem *item = dynamic_cast<SPItem *>(i);
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ // Scale by inverse of accumulated parent transform
+ SPCSSAttr *css_set = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_set, css);
+ Geom::Affine const local(item->i2doc_affine());
+ double const ex(local.descrim());
+ if ((ex != 0.0) && (ex != 1.0)) {
+ sp_css_attr_scale(css_set, 1 / ex);
+ }
+ recursively_set_properties(item, css_set);
+ sp_repr_css_attr_unref(css_set);
+ }
+ }
+ } else {
+ // Apply css to selected inner objects.
+ sp_desktop_set_style (desktop, css, true, false);
+ }
+}
+
+void
+TextToolbar::fontfamily_value_changed()
+{
+#ifdef DEBUG_TEXT
+ std::cout << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl;
+ std::cout << "sp_text_fontfamily_value_changed: " << std::endl;
+#endif
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+#ifdef DEBUG_TEXT
+ std::cout << "sp_text_fontfamily_value_changed: frozen... return" << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n" << std::endl;
+#endif
+ return;
+ }
+ _freeze = true;
+
+ gchar *temp_family = _font_family_item->get_active_text();
+ Glib::ustring new_family(temp_family);
+ g_free(temp_family);
+ css_font_family_unquote( new_family ); // Remove quotes around font family names.
+
+ // TODO: Think about how to handle handle multiple selections. While
+ // the font-family may be the same for all, the styles might be different.
+ // See: TextEdit::onApply() for example of looping over selected items.
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+#ifdef DEBUG_TEXT
+ std::cout << " Old family: " << fontlister->get_font_family() << std::endl;
+ std::cout << " New family: " << new_family << std::endl;
+ std::cout << " Old active: " << fontlister->get_font_family_row() << std::endl;
+ // std::cout << " New active: " << act->active << std::endl;
+#endif
+ if( new_family.compare( fontlister->get_font_family() ) != 0 ) {
+ // Changed font-family
+
+ if( _font_family_item->get_active() == -1 ) {
+ // New font-family, not in document, not on system (could be fallback list)
+ fontlister->insert_font_family( new_family );
+
+ // This just sets a variable in the ComboBoxEntryAction object...
+ // shouldn't we also set the actual active row in the combobox?
+ _font_family_item->set_active(0); // New family is always at top of list.
+ }
+
+ fontlister->set_font_family( _font_family_item->get_active() );
+ // active text set in sp_text_toolbox_selection_changed()
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ fontlister->fill_css( css );
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if( desktop->getSelection()->isEmpty() ) {
+ // Update default
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // If there is a selection, update
+ sp_desktop_set_style (desktop, css, true, true); // Results in selection change called twice.
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change font family"));
+ }
+ sp_repr_css_attr_unref (css);
+ }
+
+ // unfreeze
+ _freeze = false;
+
+#ifdef DEBUG_TEXT
+ std::cout << "sp_text_toolbox_fontfamily_changes: exit" << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl;
+ std::cout << std::endl;
+#endif
+}
+
+GtkWidget *
+TextToolbar::create(SPDesktop *desktop)
+{
+ auto tb = Gtk::manage(new TextToolbar(desktop));
+ return GTK_WIDGET(tb->gobj());
+}
+
+void
+TextToolbar::fontsize_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ gchar *text = _font_size_item->get_active_text();
+ gchar *endptr;
+ gdouble size = g_strtod( text, &endptr );
+ if (endptr == text) { // Conversion failed, non-numeric input.
+ g_warning( "Conversion of size text to double failed, input: %s\n", text );
+ g_free( text );
+ _freeze = false;
+ return;
+ }
+ g_free( text );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000); // somewhat arbitrary, but text&font preview freezes with too huge fontsizes
+
+ if (size > max_size)
+ size = max_size;
+
+ // Set css font size.
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ if (prefs->getBool("/options/font/textOutputPx", true)) {
+ osfs << sp_style_css_size_units_to_px(size, unit) << sp_style_get_css_unit_string(SP_CSS_UNIT_PX);
+ } else {
+ osfs << size << sp_style_get_css_unit_string(unit);
+ }
+ sp_repr_css_set_property (css, "font-size", osfs.str().c_str());
+ double factor = size / selection_fontsize;
+
+ // Apply font size to selected objects.
+ text_outer_set_style(css);
+
+ Unit const *unit_lh = _tracker->getActiveUnit();
+ g_return_if_fail(unit_lh != nullptr);
+ if (!is_relative(unit_lh) && _outer) {
+ double lineheight = _line_height_adj->get_value();
+ _freeze = false;
+ _line_height_adj->set_value(lineheight * factor);
+ _freeze = true;
+ }
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ sp_desktop_set_style(_desktop, css, true, true);
+ // Save for undo
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:size", SP_VERB_NONE,
+ _("Text: Change font size"));
+ }
+
+ sp_repr_css_attr_unref(css);
+
+ _freeze = false;
+}
+
+void
+TextToolbar::fontstyle_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ Glib::ustring new_style = _font_style_item->get_active_text();
+
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+
+ if( new_style.compare( fontlister->get_font_style() ) != 0 ) {
+
+ fontlister->set_font_style( new_style );
+ // active text set in sp_text_toolbox_seletion_changed()
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ fontlister->fill_css( css );
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ sp_desktop_set_style (desktop, css, true, true);
+
+
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_style =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTSTYLE);
+ if (result_style == QUERY_STYLE_NOTHING) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // Save for undo
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change font style"));
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ }
+
+ _freeze = false;
+}
+
+// Handles both Superscripts and Subscripts
+void
+TextToolbar::script_changed(Gtk::ToggleToolButton *btn)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ _freeze = true;
+
+ // Called by Superscript or Subscript button?
+ auto name = btn->get_name();
+ gint prop = (btn == _superscript_item) ? 0 : 1;
+
+#ifdef DEBUG_TEXT
+ std::cout << "TextToolbar::script_changed: " << prop << std::endl;
+#endif
+
+ // Query baseline
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_baseline = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_BASELINES);
+
+ bool setSuper = false;
+ bool setSub = false;
+
+ if (Inkscape::is_query_style_updateable(result_baseline)) {
+ // If not set or mixed, turn on superscript or subscript
+ if( prop == 0 ) {
+ setSuper = true;
+ } else {
+ setSub = true;
+ }
+ } else {
+ // Superscript
+ gboolean superscriptSet = (query.baseline_shift.set &&
+ query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL &&
+ query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER );
+
+ // Subscript
+ gboolean subscriptSet = (query.baseline_shift.set &&
+ query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL &&
+ query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB );
+
+ setSuper = !superscriptSet && prop == 0;
+ setSub = !subscriptSet && prop == 1;
+ }
+
+ // Set css properties
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ if( setSuper || setSub ) {
+ // Openoffice 2.3 and Adobe use 58%, Microsoft Word 2002 uses 65%, LaTex about 70%.
+ // 58% looks too small to me, especially if a superscript is placed on a superscript.
+ // If you make a change here, consider making a change to baseline-shift amount
+ // in style.cpp.
+ sp_repr_css_set_property (css, "font-size", "65%");
+ } else {
+ sp_repr_css_set_property (css, "font-size", "");
+ }
+ if( setSuper ) {
+ sp_repr_css_set_property (css, "baseline-shift", "super");
+ } else if( setSub ) {
+ sp_repr_css_set_property (css, "baseline-shift", "sub");
+ } else {
+ sp_repr_css_set_property (css, "baseline-shift", "baseline");
+ }
+
+ // Apply css to selected objects.
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ sp_desktop_set_style (desktop, css, true, false);
+
+ // Save for undo
+ if(result_baseline != QUERY_STYLE_NOTHING) {
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:script", SP_VERB_NONE,
+ _("Text: Change superscript or subscript"));
+ }
+ _freeze = false;
+}
+
+void
+TextToolbar::align_mode_changed(int mode)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/text/align_mode", mode);
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ // move the x of all texts to preserve the same bbox
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist= selection->items();
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text) {
+ SPItem *item = i;
+
+ unsigned writing_mode = item->style->writing_mode.value;
+ // below, variable names suggest horizontal move, but we check the writing direction
+ // and move in the corresponding axis
+ Geom::Dim2 axis;
+ if (writing_mode == SP_CSS_WRITING_MODE_LR_TB || writing_mode == SP_CSS_WRITING_MODE_RL_TB) {
+ axis = Geom::X;
+ } else {
+ axis = Geom::Y;
+ }
+
+ Geom::OptRect bbox = item->geometricBounds();
+ if (!bbox)
+ continue;
+ double width = bbox->dimensions()[axis];
+ // If you want to align within some frame, other than the text's own bbox, calculate
+ // the left and right (or top and bottom for tb text) slacks of the text inside that
+ // frame (currently unused)
+ double left_slack = 0;
+ double right_slack = 0;
+ unsigned old_align = item->style->text_align.value;
+ double move = 0;
+ if (old_align == SP_CSS_TEXT_ALIGN_START || old_align == SP_CSS_TEXT_ALIGN_LEFT) {
+ switch (mode) {
+ case 0:
+ move = -left_slack;
+ break;
+ case 1:
+ move = width/2 + (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = width + right_slack;
+ break;
+ }
+ } else if (old_align == SP_CSS_TEXT_ALIGN_CENTER) {
+ switch (mode) {
+ case 0:
+ move = -width/2 - left_slack;
+ break;
+ case 1:
+ move = (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = width/2 + right_slack;
+ break;
+ }
+ } else if (old_align == SP_CSS_TEXT_ALIGN_END || old_align == SP_CSS_TEXT_ALIGN_RIGHT) {
+ switch (mode) {
+ case 0:
+ move = -width - left_slack;
+ break;
+ case 1:
+ move = -width/2 + (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = right_slack;
+ break;
+ }
+ }
+ Geom::Point XY = SP_TEXT(item)->attributes.firstXY();
+ if (axis == Geom::X) {
+ XY = XY + Geom::Point (move, 0);
+ } else {
+ XY = XY + Geom::Point (0, move);
+ }
+ SP_TEXT(item)->attributes.setFirstXY(XY);
+ item->updateRepr();
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+ }
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ switch (mode)
+ {
+ case 0:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "start");
+ sp_repr_css_set_property (css, "text-align", "start");
+ break;
+ }
+ case 1:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "middle");
+ sp_repr_css_set_property (css, "text-align", "center");
+ break;
+ }
+
+ case 2:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "end");
+ sp_repr_css_set_property (css, "text-align", "end");
+ break;
+ }
+
+ case 3:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "start");
+ sp_repr_css_set_property (css, "text-align", "justify");
+ break;
+ }
+ }
+
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (desktop, css, true, true);
+ if (result_numbers != QUERY_STYLE_NOTHING)
+ {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change alignment"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas));
+
+ _freeze = false;
+}
+
+void
+TextToolbar::writing_mode_changed(int mode)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ switch (mode)
+ {
+ case 0:
+ {
+ sp_repr_css_set_property (css, "writing-mode", "lr-tb");
+ break;
+ }
+
+ case 1:
+ {
+ sp_repr_css_set_property (css, "writing-mode", "tb-rl");
+ break;
+ }
+
+ case 2:
+ {
+ sp_repr_css_set_property (css, "writing-mode", "vertical-lr");
+ break;
+ }
+ }
+
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change writing mode"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas));
+
+ _freeze = false;
+}
+
+void
+TextToolbar::orientation_changed(int mode)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ switch (mode)
+ {
+ case 0:
+ {
+ sp_repr_css_set_property (css, "text-orientation", "auto");
+ break;
+ }
+
+ case 1:
+ {
+ sp_repr_css_set_property (css, "text-orientation", "upright");
+ break;
+ }
+
+ case 2:
+ {
+ sp_repr_css_set_property (css, "text-orientation", "sideways");
+ break;
+ }
+ }
+
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change orientation"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas));
+
+ _freeze = false;
+}
+
+void
+TextToolbar::direction_changed(int mode)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ switch (mode)
+ {
+ case 0:
+ {
+ sp_repr_css_set_property (css, "direction", "ltr");
+ break;
+ }
+
+ case 1:
+ {
+ sp_repr_css_set_property (css, "direction", "rtl");
+ break;
+ }
+ }
+
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Text: Change direction"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas));
+
+ _freeze = false;
+}
+
+void
+TextToolbar::lineheight_value_changed()
+{
+ // quit if run by the _changed callbacks or is not text tool
+ if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) {
+ return;
+ }
+
+ _freeze = true;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ // Get user selected unit and save as preference
+ Unit const *unit = _tracker->getActiveUnit();
+ // @Tav same disabled unit
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit so
+ // we can save it (allows us to adjust line height value when unit changes).
+
+ // Set css line height.
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ if ( is_relative(unit) ) {
+ osfs << _line_height_adj->get_value() << unit->abbr;
+ } else {
+ // Inside SVG file, always use "px" for absolute units.
+ osfs << Quantity::convert(_line_height_adj->get_value(), unit, "px") << "px";
+ }
+
+ sp_repr_css_set_property (css, "line-height", osfs.str().c_str());
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist = selection->items();
+ if (_outer) {
+ // Special else makes this different from other uses of text_outer_set_style
+ text_outer_set_style(css);
+ } else {
+ SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin());
+ SPStyle *parent_style = parent->style;
+ SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET);
+ Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25");
+ SPCSSAttr *cssfit = sp_repr_css_attr_new();
+ sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str());
+ double minheight = 0;
+ if (parent_style) {
+ minheight = parent_style->line_height.computed;
+ }
+ if (minheight) {
+ for (auto i : parent->childList(false)) {
+ SPItem *child = dynamic_cast<SPItem *>(i);
+ if (!child) {
+ continue;
+ }
+ recursively_set_properties(child, cssfit);
+ }
+ }
+ sp_repr_css_set_property(cssfit, "line-height", "0");
+ parent->changeCSS(cssfit, "style");
+ subselection_wrap_toggle(true);
+ sp_desktop_set_style(desktop, css, true, true);
+ subselection_wrap_toggle(false);
+ sp_repr_css_attr_unref(cssfit);
+ }
+ // Only need to save for undo if a text item has been changed.
+ itemlist = selection->items();
+ bool modmade = false;
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ modmade = true;
+ break;
+ }
+ }
+
+ // Save for undo
+ if (modmade) {
+ // Call ensureUpToDate() causes rebuild of text layout (with all proper style
+ // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly
+ // save new <tspan> 'x' and 'y' attribute values by calling updateRepr().
+ // Partial fix for bug #1590141.
+
+ desktop->getDocument()->ensureUpToDate();
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (!_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(desktop->getDocument(), "ttb:line-height", SP_VERB_NONE, _("Text: Change line-height"));
+ }
+
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void
+TextToolbar::lineheight_unit_changed(int /* Not Used */)
+{
+ // quit if run by the _changed callbacks or is not text tool
+ if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) {
+ return;
+ }
+ _freeze = true;
+
+ // Get old saved unit
+ int old_unit = _lineheight_unit;
+
+ // Get user selected unit and save as preference
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit.
+ SPILength temp_length;
+ Inkscape::CSSOStringStream temp_stream;
+ temp_stream << 1 << unit->abbr;
+ temp_length.read(temp_stream.str().c_str());
+ prefs->setInt("/tools/text/lineheight/display_unit", temp_length.unit);
+ if (old_unit == temp_length.unit) {
+ _freeze = false;
+ return;
+ }
+
+ // Read current line height value
+ double line_height = _line_height_adj->get_value();
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist = selection->items();
+
+ // Convert between units
+ double font_size = 0;
+ double doc_scale = 1;
+ int count = 0;
+ bool has_flow = false;
+
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ doc_scale = Geom::Affine(i->i2dt_affine()).descrim();
+ font_size += i->style->font_size.computed * doc_scale;
+ ++count;
+ }
+ if (flowtext) {
+ has_flow = true;
+ }
+ }
+ if (count > 0) {
+ font_size /= count;
+ } else {
+ font_size = 20;
+ }
+ if ((unit->abbr == "" || unit->abbr == "em") && (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_EM)) {
+ // Do nothing
+ } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_EX) {
+ line_height *= 0.5;
+ } else if ((unit->abbr) == "ex" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) {
+ line_height *= 2.0;
+ } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_PERCENT) {
+ line_height /= 100.0;
+ } else if ((unit->abbr) == "%" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) {
+ line_height *= 100;
+ } else if ((unit->abbr) == "ex" && old_unit == SP_CSS_UNIT_PERCENT) {
+ line_height /= 50.0;
+ } else if ((unit->abbr) == "%" && old_unit == SP_CSS_UNIT_EX) {
+ line_height *= 50;
+ } else if (is_relative(unit)) {
+ // Convert absolute to relative... for the moment use average font-size
+ if (old_unit == SP_CSS_UNIT_NONE) old_unit = SP_CSS_UNIT_EM;
+ line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), "px");
+
+ if (font_size > 0) {
+ line_height /= font_size;
+ }
+ if ((unit->abbr) == "%") {
+ line_height *= 100;
+ } else if ((unit->abbr) == "ex") {
+ line_height *= 2;
+ }
+ } else if (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_PERCENT || old_unit == SP_CSS_UNIT_EM ||
+ old_unit == SP_CSS_UNIT_EX) {
+ // Convert relative to absolute... for the moment use average font-size
+ if (old_unit == SP_CSS_UNIT_PERCENT) {
+ line_height /= 100.0;
+ } else if (old_unit == SP_CSS_UNIT_EX) {
+ line_height /= 2.0;
+ }
+ line_height *= font_size;
+ line_height = Quantity::convert(line_height, "px", unit);
+ } else {
+ // Convert between different absolute units (only used in GUI)
+ line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), unit);
+ }
+ // Set css line height.
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ // Set css line height.
+ if ( is_relative(unit) ) {
+ osfs << line_height << unit->abbr;
+ } else {
+ osfs << Quantity::convert(line_height, unit, "px") << "px";
+ }
+ sp_repr_css_set_property (css, "line-height", osfs.str().c_str());
+
+ // Update GUI with line_height value.
+ _line_height_adj->set_value(line_height);
+ // Update "climb rate" The custom action has a step property but no way to set it.
+ if (unit->abbr == "%") {
+ _line_height_adj->set_step_increment(1.0);
+ _line_height_adj->set_page_increment(10.0);
+ } else {
+ _line_height_adj->set_step_increment(0.1);
+ _line_height_adj->set_page_increment(1.0);
+ }
+ // Internal function to set line-height which is spacing mode dependent.
+ if (_outer) {
+ for (auto i = itemlist.begin(); i != itemlist.end(); ++i) {
+ if (dynamic_cast<SPText *>(*i) || dynamic_cast<SPFlowtext *>(*i)) {
+ SPItem *item = *i;
+ // Scale by inverse of accumulated parent transform
+ SPCSSAttr *css_set = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_set, css);
+ Geom::Affine const local(item->i2doc_affine());
+ double const ex(local.descrim());
+ if ((ex != 0.0) && (ex != 1.0)) {
+ sp_css_attr_scale(css_set, 1 / ex);
+ }
+ recursively_set_properties(item, css_set);
+ sp_repr_css_attr_unref(css_set);
+ }
+ }
+ } else {
+ SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin());
+ SPStyle *parent_style = parent->style;
+ SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET);
+ Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25");
+ SPCSSAttr *cssfit = sp_repr_css_attr_new();
+ sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str());
+ double minheight = 0;
+ if (parent_style) {
+ minheight = parent_style->line_height.computed;
+ }
+ if (minheight) {
+ for (auto i : parent->childList(false)) {
+ SPItem *child = dynamic_cast<SPItem *>(i);
+ if (!child) {
+ continue;
+ }
+ recursively_set_properties(child, cssfit);
+ }
+ }
+ sp_repr_css_set_property(cssfit, "line-height", "0");
+ parent->changeCSS(cssfit, "style");
+ subselection_wrap_toggle(true);
+ sp_desktop_set_style(desktop, css, true, true);
+ subselection_wrap_toggle(false);
+ sp_repr_css_attr_unref(cssfit);
+ }
+ itemlist= selection->items();
+ // Only need to save for undo if a text item has been changed.
+ bool modmade = false;
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ modmade = true;
+ break;
+ }
+ }
+ // Save for undo
+ if(modmade) {
+ // Call ensureUpToDate() causes rebuild of text layout (with all proper style
+ // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly
+ // save new <tspan> 'x' and 'y' attribute values by calling updateRepr().
+ // Partial fix for bug #1590141.
+
+ desktop->getDocument()->ensureUpToDate();
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:line-height", SP_VERB_NONE,
+ _("Text: Change line-height unit"));
+ }
+
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void TextToolbar::fontsize_unit_changed(int /* Not Used */)
+{
+ // quit if run by the _changed callbacks
+ Unit const *unit = _tracker_fs->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit.
+ SPILength temp_size;
+ Inkscape::CSSOStringStream temp_size_stream;
+ temp_size_stream << 1 << unit->abbr;
+ temp_size.read(temp_size_stream.str().c_str());
+ prefs->setInt("/options/font/unitType", temp_size.unit);
+ selection_changed(SP_ACTIVE_DESKTOP->selection);
+}
+
+void
+TextToolbar::wordspacing_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ // At the moment this handles only numerical values (i.e. no em unit).
+ // Set css word-spacing
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ osfs << _word_spacing_adj->get_value() << "px"; // For now always use px
+ sp_repr_css_set_property (css, "word-spacing", osfs.str().c_str());
+ text_outer_set_style(css);
+
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // Save for undo
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:word-spacing", SP_VERB_NONE,
+ _("Text: Change word-spacing"));
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void
+TextToolbar::letterspacing_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ // At the moment this handles only numerical values (i.e. no em unit).
+ // Set css letter-spacing
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ osfs << _letter_spacing_adj->get_value() << "px"; // For now always use px
+ sp_repr_css_set_property (css, "letter-spacing", osfs.str().c_str());
+ text_outer_set_style(css);
+
+ // If no selected objects, set default.
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ int result_numbers =
+ sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+ else
+ {
+ // Save for undo
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:letter-spacing", SP_VERB_NONE,
+ _("Text: Change letter-spacing"));
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void
+TextToolbar::dx_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ gdouble new_dx = _dx_adj->get_value();
+ bool modmade = false;
+
+ if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if( tc ) {
+ unsigned char_index = -1;
+ TextTagAttributes *attributes =
+ text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index );
+ if( attributes ) {
+ double old_dx = attributes->getDx( char_index );
+ double delta_dx = new_dx - old_dx;
+ sp_te_adjust_dx( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_dx );
+ modmade = true;
+ }
+ }
+ }
+
+ if(modmade) {
+ // Save for undo
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:dx", SP_VERB_NONE,
+ _("Text: Change dx (kern)"));
+ }
+ _freeze = false;
+}
+
+void
+TextToolbar::dy_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ gdouble new_dy = _dy_adj->get_value();
+ bool modmade = false;
+
+ if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if( tc ) {
+ unsigned char_index = -1;
+ TextTagAttributes *attributes =
+ text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index );
+ if( attributes ) {
+ double old_dy = attributes->getDy( char_index );
+ double delta_dy = new_dy - old_dy;
+ sp_te_adjust_dy( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_dy );
+ modmade = true;
+ }
+ }
+ }
+
+ if(modmade) {
+ // Save for undo
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:dy", SP_VERB_NONE,
+ _("Text: Change dy"));
+ }
+
+ _freeze = false;
+}
+
+void
+TextToolbar::rotation_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ gdouble new_degrees = _rotation_adj->get_value();
+
+ bool modmade = false;
+ if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if( tc ) {
+ unsigned char_index = -1;
+ TextTagAttributes *attributes =
+ text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index );
+ if( attributes ) {
+ double old_degrees = attributes->getRotate( char_index );
+ double delta_deg = new_degrees - old_degrees;
+ sp_te_adjust_rotation( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_deg );
+ modmade = true;
+ }
+ }
+ }
+
+ // Save for undo
+ if(modmade) {
+ DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:rotate", SP_VERB_NONE,
+ _("Text: Change rotate"));
+ }
+
+ _freeze = false;
+}
+
+void TextToolbar::selection_modified_select_tool(Inkscape::Selection *selection, guint flags)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double factor = prefs->getDouble("/options/font/scaleLineHeightFromFontSIze", 1.0);
+ if (factor != 1.0) {
+ Unit const *unit_lh = _tracker->getActiveUnit();
+ g_return_if_fail(unit_lh != nullptr);
+ if (!is_relative(unit_lh) && _outer) {
+ double lineheight = _line_height_adj->get_value();
+ bool is_freeze = _freeze;
+ _freeze = false;
+ _line_height_adj->set_value(lineheight * factor);
+ _freeze = is_freeze;
+ }
+ prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", 1.0);
+ }
+}
+
+void TextToolbar::selection_changed(Inkscape::Selection *selection) // don't bother to update font list if subsel
+ // changed
+{
+#ifdef DEBUG_TEXT
+ static int count = 0;
+ ++count;
+ std::cout << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << "sp_text_toolbox_selection_changed: start " << count << std::endl;
+#endif
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+
+#ifdef DEBUG_TEXT
+ std::cout << " Frozen, returning" << std::endl;
+ std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << std::endl;
+#endif
+ return;
+ }
+ _freeze = true;
+
+ // selection defined as argument but not used, argh!!!
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ selection = desktop->getSelection();
+ auto itemlist = selection->items();
+
+#ifdef DEBUG_TEXT
+ for(auto i : itemlist) {
+ const gchar* id = i->getId();
+ std::cout << " " << id << std::endl;
+ }
+ Glib::ustring selected_text = sp_text_get_selected_text((SP_ACTIVE_DESKTOP)->event_context);
+ std::cout << " Selected text: |" << selected_text << "|" << std::endl;
+#endif
+
+ // Only flowed text can be justified, only normal text can be kerned...
+ // Find out if we have flowed text now so we can use it several places
+ gboolean isFlow = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::vector<SPItem *> to_work;
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ to_work.push_back(i);
+ }
+ if (flowtext ||
+ (text && text->style && text->style->shape_inside.set)) {
+ isFlow = true;
+ }
+ }
+ bool outside = false;
+ if (selection && to_work.size() == 0) {
+ outside = true;
+ }
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->selection_update();
+ // Update font list, but only if widget already created.
+ if (_font_family_item->get_combobox() != nullptr) {
+ _font_family_item->set_active_text(fontlister->get_font_family().c_str(), fontlister->get_font_family_row());
+ _font_style_item->set_active_text(fontlister->get_font_style().c_str());
+ }
+
+ /*
+ * Query from current selection:
+ * Font family (font-family)
+ * Style (font-weight, font-style, font-stretch, font-variant, font-align)
+ * Numbers (font-size, letter-spacing, word-spacing, line-height, text-anchor, writing-mode)
+ * Font specification (Inkscape private attribute)
+ */
+ SPStyle query(document);
+ SPStyle query_fallback(document);
+ int result_family = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTFAMILY);
+ int result_style = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE);
+ int result_baseline = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_BASELINES);
+ int result_wmode = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // Calling sp_desktop_query_style will result in a call to TextTool::_styleQueried().
+ // This returns the style of the selected text inside the <text> element... which
+ // is often the style of one or more <tspan>s. If we want the style of the outer
+ // <text> objects then we need to bypass the call to TextTool::_styleQueried().
+ // The desktop selection never includes the elements inside the <text> element.
+ int result_numbers = 0;
+ int result_numbers_fallback = 0;
+ if (!outside) {
+ if (_outer && this->_sub_active_item) {
+ std::vector<SPItem *> qactive{ this->_sub_active_item };
+ SPItem *parent = dynamic_cast<SPItem *>(this->_sub_active_item->parent);
+ std::vector<SPItem *> qparent{ parent };
+ result_numbers =
+ sp_desktop_query_style_from_list(qactive, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ result_numbers_fallback =
+ sp_desktop_query_style_from_list(qparent, &query_fallback, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ } else if (_outer) {
+ result_numbers = sp_desktop_query_style_from_list(to_work, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ } else {
+ result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ }
+ } else {
+ result_numbers =
+ sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ }
+
+ /*
+ * If no text in selection (querying returned nothing), read the style from
+ * the /tools/text preferences (default style for new texts). Return if
+ * tool bar already set to these preferences.
+ */
+ if (result_family == QUERY_STYLE_NOTHING ||
+ result_style == QUERY_STYLE_NOTHING ||
+ result_numbers == QUERY_STYLE_NOTHING ||
+ result_wmode == QUERY_STYLE_NOTHING ) {
+ // There are no texts in selection, read from preferences.
+ query.readFromPrefs("/tools/text");
+#ifdef DEBUG_TEXT
+ std::cout << " read style from prefs:" << std::endl;
+ sp_print_font( &query );
+#endif
+ if (_text_style_from_prefs) {
+ // Do not reset the toolbar style from prefs if we already did it last time
+ _freeze = false;
+#ifdef DEBUG_TEXT
+ std::cout << " text_style_from_prefs: toolbar already set" << std:: endl;
+ std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << std::endl;
+#endif
+ return;
+ }
+
+ // To ensure the value of the combobox is properly set on start-up, only mark
+ // the prefs set if the combobox has already been constructed.
+ if( _font_family_item->get_combobox() != nullptr ) {
+ _text_style_from_prefs = true;
+ }
+ } else {
+ _text_style_from_prefs = false;
+ }
+
+ // If we have valid query data for text (font-family, font-specification) set toolbar accordingly.
+ {
+ // Size (average of text selected)
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double size = 0;
+ if (!size && _cusor_numbers != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(_query_cursor.font_size.computed, unit);
+ }
+ if (!size && result_numbers != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(query.font_size.computed, unit);
+ }
+ if (!size && result_numbers_fallback != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(query_fallback.font_size.computed, unit);
+ }
+
+ auto unit_str = sp_style_get_css_unit_string(unit);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")");
+
+ _font_size_item->set_tooltip(tooltip.c_str());
+
+ Inkscape::CSSOStringStream os;
+ // We dot want to parse values just show
+
+ _tracker_fs->setActiveUnitByAbbr(sp_style_get_css_unit_string(unit));
+ int rounded_size = std::round(size);
+ if (std::abs((size - rounded_size)/size) < 0.0001) {
+ // We use rounded_size to avoid rounding errors when, say, converting stored 'px' values to displayed 'pt' values.
+ os << rounded_size;
+ selection_fontsize = rounded_size;
+ } else {
+ os << size;
+ selection_fontsize = size;
+ }
+
+ // Freeze to ignore callbacks.
+ //g_object_freeze_notify( G_OBJECT( fontSizeAction->combobox ) );
+ sp_text_set_sizes(GTK_LIST_STORE(_font_size_item->get_model()), unit);
+ //g_object_thaw_notify( G_OBJECT( fontSizeAction->combobox ) );
+
+ _font_size_item->set_active_text( os.str().c_str() );
+
+ // Superscript
+ gboolean superscriptSet =
+ ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) &&
+ query.baseline_shift.set &&
+ query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL &&
+ query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER );
+
+ _superscript_item->set_active(superscriptSet);
+
+ // Subscript
+ gboolean subscriptSet =
+ ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) &&
+ query.baseline_shift.set &&
+ query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL &&
+ query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB );
+
+ _subscript_item->set_active(subscriptSet);
+
+ // Alignment
+
+ // Note: SVG 1.1 doesn't include text-align, SVG 1.2 Tiny doesn't include text-align="justify"
+ // text-align="justify" was a draft SVG 1.2 item (along with flowed text).
+ // Only flowed text can be left and right justified at the same time.
+ // Disable button if we don't have flowed text.
+
+ Glib::RefPtr<Gtk::ListStore> store = _align_item->get_store();
+ Gtk::TreeModel::Row row = *(store->get_iter("3")); // Justify entry
+ UI::Widget::ComboToolItemColumns columns;
+ row[columns.col_sensitive] = isFlow;
+
+ int activeButton = 0;
+ if (query.text_align.computed == SP_CSS_TEXT_ALIGN_JUSTIFY)
+ {
+ activeButton = 3;
+ } else {
+ // This should take 'direction' into account
+ if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_START) activeButton = 0;
+ if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_MIDDLE) activeButton = 1;
+ if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_END) activeButton = 2;
+ }
+ _align_item->set_active( activeButton );
+
+ double height = 0;
+ gint line_height_unit = 0;
+
+ if (!height && _cusor_numbers != QUERY_STYLE_NOTHING) {
+ height = _query_cursor.line_height.value;
+ line_height_unit = _query_cursor.line_height.unit;
+ }
+
+ if (!height && result_numbers != QUERY_STYLE_NOTHING) {
+ height = query.line_height.value;
+ line_height_unit = query.line_height.unit;
+ }
+
+ if (!height && result_numbers_fallback != QUERY_STYLE_NOTHING) {
+ height = query_fallback.line_height.value;
+ line_height_unit = query_fallback.line_height.unit;
+ }
+
+ if (line_height_unit == SP_CSS_UNIT_PERCENT) {
+ height *= 100.0; // Inkscape store % as fraction in .value
+ }
+
+ // We dot want to parse values just show
+ if (!is_relative(SPCSSUnit(line_height_unit))) {
+ gint curunit = prefs->getInt("/tools/text/lineheight/display_unit", 1);
+ // For backwards comaptibility
+ if (is_relative(SPCSSUnit(curunit))) {
+ prefs->setInt("/tools/text/lineheight/display_unit", 1);
+ curunit = 1;
+ }
+ height = Quantity::convert(height, "px", sp_style_get_css_unit_string(curunit));
+ line_height_unit = curunit;
+ }
+ _line_height_adj->set_value(height);
+
+
+ // Update "climb rate"
+ if (line_height_unit == SP_CSS_UNIT_PERCENT) {
+ _line_height_adj->set_step_increment(1.0);
+ _line_height_adj->set_page_increment(10.0);
+ } else {
+ _line_height_adj->set_step_increment(0.1);
+ _line_height_adj->set_page_increment(1.0);
+ }
+
+ if( line_height_unit == SP_CSS_UNIT_NONE ) {
+ // Function 'sp_style_get_css_unit_string' returns 'px' for unit none.
+ // We need to avoid this.
+ _tracker->setActiveUnitByAbbr("");
+ } else {
+ _tracker->setActiveUnitByAbbr(sp_style_get_css_unit_string(line_height_unit));
+ }
+
+ // Save unit so we can do conversions between new/old units.
+ _lineheight_unit = line_height_unit;
+ // Word spacing
+ double wordSpacing;
+ if (query.word_spacing.normal) wordSpacing = 0.0;
+ else wordSpacing = query.word_spacing.computed; // Assume no units (change in desktop-style.cpp)
+
+ _word_spacing_adj->set_value(wordSpacing);
+
+ // Letter spacing
+ double letterSpacing;
+ if (query.letter_spacing.normal) letterSpacing = 0.0;
+ else letterSpacing = query.letter_spacing.computed; // Assume no units (change in desktop-style.cpp)
+
+ _letter_spacing_adj->set_value(letterSpacing);
+
+ // Writing mode
+ int activeButton2 = 0;
+ if (query.writing_mode.computed == SP_CSS_WRITING_MODE_LR_TB) activeButton2 = 0;
+ if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_RL) activeButton2 = 1;
+ if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_LR) activeButton2 = 2;
+
+ _writing_mode_item->set_active( activeButton2 );
+
+ // Orientation
+ int activeButton3 = 0;
+ if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_MIXED ) activeButton3 = 0;
+ if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_UPRIGHT ) activeButton3 = 1;
+ if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_SIDEWAYS) activeButton3 = 2;
+
+ _orientation_item->set_active( activeButton3 );
+
+ // Disable text orientation for horizontal text...
+ _orientation_item->set_sensitive( activeButton2 != 0 );
+
+ // Direction
+ int activeButton4 = 0;
+ if (query.direction.computed == SP_CSS_DIRECTION_LTR ) activeButton4 = 0;
+ if (query.direction.computed == SP_CSS_DIRECTION_RTL ) activeButton4 = 1;
+ _direction_item->set_active( activeButton4 );
+ }
+
+#ifdef DEBUG_TEXT
+ std::cout << " GUI: fontfamily.value: " << query.font_family.value() << std::endl;
+ std::cout << " GUI: font_size.computed: " << query.font_size.computed << std::endl;
+ std::cout << " GUI: font_weight.computed: " << query.font_weight.computed << std::endl;
+ std::cout << " GUI: font_style.computed: " << query.font_style.computed << std::endl;
+ std::cout << " GUI: text_anchor.computed: " << query.text_anchor.computed << std::endl;
+ std::cout << " GUI: text_align.computed: " << query.text_align.computed << std::endl;
+ std::cout << " GUI: line_height.computed: " << query.line_height.computed
+ << " line_height.value: " << query.line_height.value
+ << " line_height.unit: " << query.line_height.unit << std::endl;
+ std::cout << " GUI: word_spacing.computed: " << query.word_spacing.computed
+ << " word_spacing.value: " << query.word_spacing.value
+ << " word_spacing.unit: " << query.word_spacing.unit << std::endl;
+ std::cout << " GUI: letter_spacing.computed: " << query.letter_spacing.computed
+ << " letter_spacing.value: " << query.letter_spacing.value
+ << " letter_spacing.unit: " << query.letter_spacing.unit << std::endl;
+ std::cout << " GUI: writing_mode.computed: " << query.writing_mode.computed << std::endl;
+#endif
+
+ // Kerning (xshift), yshift, rotation. NB: These are not CSS attributes.
+ if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if( tc ) {
+ unsigned char_index = -1;
+ TextTagAttributes *attributes =
+ text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index );
+ if( attributes ) {
+
+ // Dx
+ double dx = attributes->getDx( char_index );
+ _dx_adj->set_value(dx);
+
+ // Dy
+ double dy = attributes->getDy( char_index );
+ _dy_adj->set_value(dy);
+
+ // Rotation
+ double rotation = attributes->getRotate( char_index );
+ /* SVG value is between 0 and 360 but we're using -180 to 180 in widget */
+ if( rotation > 180.0 ) rotation -= 360.0;
+ _rotation_adj->set_value(rotation);
+
+#ifdef DEBUG_TEXT
+ std::cout << " GUI: Dx: " << dx << std::endl;
+ std::cout << " GUI: Dy: " << dy << std::endl;
+ std::cout << " GUI: Rotation: " << rotation << std::endl;
+#endif
+ }
+ }
+ }
+
+ {
+ // Set these here as we don't always have kerning/rotating attributes
+ _dx_item->set_sensitive(!isFlow);
+ _dy_item->set_sensitive(!isFlow);
+ _rotation_item->set_sensitive(!isFlow);
+ }
+
+#ifdef DEBUG_TEXT
+ std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << std::endl;
+#endif
+
+ _freeze = false;
+}
+
+void
+TextToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) {
+ bool is_text_toolbar = SP_IS_TEXT_CONTEXT(ec);
+ bool is_select_toolbar = !is_text_toolbar && SP_IS_SELECT_CONTEXT(ec);
+ if (is_text_toolbar) {
+ // Watch selection
+ // Ensure FontLister is updated here first..................
+ c_selection_changed =
+ desktop->getSelection()->connectChangedFirst(sigc::mem_fun(*this, &TextToolbar::selection_changed));
+ c_selection_modified = desktop->getSelection()->connectModifiedFirst(sigc::mem_fun(*this, &TextToolbar::selection_modified));
+ c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &TextToolbar::subselection_changed));
+ this->_sub_active_item = nullptr;
+ selection_changed(desktop->getSelection());
+ } else if (is_select_toolbar) {
+ c_selection_modified_select_tool = desktop->getSelection()->connectModifiedFirst(
+ sigc::mem_fun(*this, &TextToolbar::selection_modified_select_tool));
+ }
+
+
+ if (!is_text_toolbar) {
+ c_selection_changed.disconnect();
+ c_selection_modified.disconnect();
+ c_subselection_changed.disconnect();
+ }
+
+ if (!is_select_toolbar) {
+ c_selection_modified_select_tool.disconnect();
+ }
+}
+
+void
+TextToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ this->_sub_active_item = nullptr;
+ selection_changed(selection);
+
+}
+
+void TextToolbar::subselection_wrap_toggle(bool start)
+{
+ if (SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context)) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if (tc) {
+ _updating = true;
+ Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
+ if (layout) {
+ Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start;
+ Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end;
+ tc->text_sel_start = wrap_start;
+ tc->text_sel_end = wrap_end;
+ wrap_start = start_selection;
+ wrap_end = end_selection;
+ }
+ _updating = start;
+ }
+ }
+}
+
+/*
+* This function parse the just created line height in one or more lines of a text subselection
+* can recibe 2 kinds of input because when we store a text element we apply a fallback that change
+* structure. This visualy is not reflected but user maybe want to change a part of this subselection
+* once the falback is created, so we need more complex here to fill the gap.
+* Basicaly we have a line height changed in the new wraper element/s between wrap_start and wrap_end
+* this variables store starting iterator of first char in line and last char in line in a subselection
+* this elements are styled well but we can have orphands text nodes before and after the subselection.
+* this normaly 3 elements are inside a container as direct child of a text element, so we need to apply
+* the container style to the optional previous and last text nodes warping into a new element that get the
+* container style (this are not part to the subsselection). After wrap we unindent all child of the container and
+* remove the container
+*
+*/
+void TextToolbar::prepare_inner()
+{
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context);
+ if (tc) {
+ Inkscape::Text::Layout *layout = const_cast<Inkscape::Text::Layout *>(te_get_layout(tc->text));
+ if (layout) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ SPObject *spobject = dynamic_cast<SPObject *>(tc->text);
+ SPItem *spitem = dynamic_cast<SPItem *>(tc->text);
+ SPText *text = dynamic_cast<SPText *>(tc->text);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text);
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ if (!spobject) {
+ return;
+ }
+
+ // We check for external files with text nodes direct children of text elemet
+ // and wrap it into a tspan elements as inkscape do.
+ if (text) {
+ std::vector<SPObject *> childs = spitem->childList(false);
+ for (auto child : childs) {
+ SPString *spstring = dynamic_cast<SPString *>(child);
+ if (spstring) {
+ Glib::ustring content = spstring->string;
+ if (content != "\n") {
+ Inkscape::XML::Node *rstring = xml_doc->createTextNode(content.c_str());
+ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
+ Inkscape::XML::Node *rnl = xml_doc->createTextNode("\n");
+ rtspan->setAttribute("sodipodi:role", "line");
+ rtspan->addChild(rstring, nullptr);
+ text->getRepr()->addChild(rtspan, child->getRepr());
+ Inkscape::GC::release(rstring);
+ Inkscape::GC::release(rtspan);
+ text->getRepr()->removeChild(spstring->getRepr());
+ }
+ }
+ }
+ }
+
+ // Here we remove temporary the shape to allow layout calculate where are the warp_end and warpo_start
+ // position if one of this are hiden because the previous line height changed
+ if (text) {
+ text->hide_shape_inside();
+ }
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(false);
+ }
+ SPObject *rawptr_start = nullptr;
+ SPObject *rawptr_end = nullptr;
+ layout->getSourceOfCharacter(wrap_start, &rawptr_start);
+ layout->getSourceOfCharacter(wrap_end, &rawptr_end);
+ if (text) {
+ text->show_shape_inside();
+ }
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(true);
+ }
+ if (!rawptr_start || !rawptr_end || !SP_IS_OBJECT(rawptr_start)|| !SP_IS_OBJECT(rawptr_end)) {
+ return;
+ }
+ SPObject *startobj = reinterpret_cast<SPObject *>(rawptr_start);
+ SPObject *endobj = reinterpret_cast<SPObject *>(rawptr_end);
+ // here we try to slect the elements where we need to work
+ // looping parent while text element in start and end
+ // and geting this and the inbetween elements
+ SPObject *start = startobj;
+ SPObject *end = endobj;
+ while (start->parent != spobject)
+ {
+ start = start->parent;
+ }
+ while (end->parent != spobject)
+ {
+ end = end->parent;
+ }
+ std::vector<SPObject *> containers;
+ while (start && start != end) {
+ containers.push_back(start);
+ start = start->getNext();
+ }
+ if (start) {
+ containers.push_back(start);
+ }
+ for (auto container : containers) {
+ //we store the parent style to apply to the childs unselected
+ const gchar * style = container->getRepr()->attribute("style");
+ Inkscape::XML::Node *prevchild = container->getRepr();
+ std::vector<SPObject*> childs = container->childList(false);
+ for (auto child : childs) {
+ SPString *spstring = dynamic_cast<SPString *>(child);
+ SPFlowtspan *flowtspan = dynamic_cast<SPFlowtspan *>(child);
+ SPTSpan *tspan = dynamic_cast<SPTSpan *>(child);
+ // we need to upper all flowtspans to container level
+ // to this we need to change the element from flowspan to flowpara
+ if (flowtspan) {
+ Inkscape::XML::Node *flowpara = xml_doc->createElement("svg:flowPara");
+ std::vector<SPObject*> fts_childs = flowtspan->childList(false);
+ bool hascontent = false;
+ // we need to move the contents to the new created element
+ // mayve we can move directly but the safer for me is duplicate
+ // inject into the new element and delete original
+ for (auto fts_child : fts_childs) {
+ // is this check necesary?
+ if (fts_child) {
+ Inkscape::XML::Node *fts_child_node = fts_child->getRepr()->duplicate(xml_doc);
+ flowtspan->getRepr()->removeChild(fts_child->getRepr());
+ flowpara->addChild(fts_child_node, nullptr);
+ Inkscape::GC::release(fts_child_node);
+ hascontent = true;
+ }
+ }
+ // if no contents we dont want to add
+ if (hascontent) {
+ flowpara->setAttribute("style", flowtspan->getRepr()->attribute("style"));
+ spobject->getRepr()->addChild(flowpara, prevchild);
+ Inkscape::GC::release(flowpara);
+ prevchild = flowpara;
+ }
+ container->getRepr()->removeChild(flowtspan->getRepr());
+ } else if (tspan) {
+ if (child->childList(false).size()) {
+ child->getRepr()->setAttribute("sodipodi:role", "line");
+ // maybe we need to move unindent function here
+ // to be the same as other here
+ prevchild = unindent_node(child->getRepr(), prevchild);
+ } else {
+ // if no contents we dont want to add
+ container->getRepr()->removeChild(child->getRepr());
+ }
+ } else if (spstring) {
+ // we are on a text node, we act diferent if in a text or flowtext.
+ // wrap a duplicate of the element and unindent after the prevchild
+ // and finaly delete original
+ Inkscape::XML::Node *string_node = xml_doc->createTextNode(spstring->string.c_str());
+ if (text) {
+ Inkscape::XML::Node *tspan_node = xml_doc->createElement("svg:tspan");
+ tspan_node->setAttribute("style", container->getRepr()->attribute("style"));
+ tspan_node->addChild(string_node, nullptr);
+ tspan_node->setAttribute("sodipodi:role", "line");
+ text->getRepr()->addChild(tspan_node, prevchild);
+ Inkscape::GC::release(string_node);
+ Inkscape::GC::release(tspan_node);
+ prevchild = tspan_node;
+ } else if (flowtext) {
+ Inkscape::XML::Node *flowpara_node = xml_doc->createElement("svg:flowPara");
+ flowpara_node->setAttribute("style", container->getRepr()->attribute("style"));
+ flowpara_node->addChild(string_node, nullptr);
+ flowtext->getRepr()->addChild(flowpara_node, prevchild);
+ Inkscape::GC::release(string_node);
+ Inkscape::GC::release(flowpara_node);
+ prevchild = flowpara_node;
+ }
+ container->getRepr()->removeChild(spstring->getRepr());
+ }
+ }
+ tc->text->getRepr()->removeChild(container->getRepr());
+ }
+ }
+ }
+}
+
+Inkscape::XML::Node *TextToolbar::unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *prevchild)
+{
+ g_assert(repr != nullptr);
+
+ Inkscape::XML::Node *parent = repr->parent();
+ if (parent) {
+ Inkscape::XML::Node *grandparent = parent->parent();
+ if (grandparent) {
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *newrepr = repr->duplicate(xml_doc);
+ parent->removeChild(repr);
+ grandparent->addChild(newrepr, prevchild);
+ Inkscape::GC::release(newrepr);
+ newrepr->setAttribute("sodipodi:role", "line");
+ return newrepr;
+ }
+ }
+ std::cout << "error on TextToolbar.cpp::2433" << std::endl;
+ return repr;
+}
+
+void TextToolbar::subselection_changed(gpointer texttool)
+{
+#ifdef DEBUG_TEXT
+ std::cout << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << "subselection_changed: start " << std::endl;
+#endif
+ // quit if run by the _changed callbacks
+ this->_sub_active_item = nullptr;
+ if (_updating) {
+ return;
+ }
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(SP_EVENT_CONTEXT(texttool));
+ if (tc) {
+ Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
+ if (layout) {
+ Inkscape::Text::Layout::iterator start = layout->begin();
+ Inkscape::Text::Layout::iterator end = layout->end();
+ Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start;
+ Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end;
+#ifdef DEBUG_TEXT
+ std::cout << " GUI: Start of text: " << layout->iteratorToCharIndex(start) << std::endl;
+ std::cout << " GUI: End of text: " << layout->iteratorToCharIndex(end) << std::endl;
+ std::cout << " GUI: Start of selection: " << layout->iteratorToCharIndex(start_selection) << std::endl;
+ std::cout << " GUI: End of selection: " << layout->iteratorToCharIndex(end_selection) << std::endl;
+ std::cout << " GUI: Loop Subelements: " << std::endl;
+ std::cout << " ::::::::::::::::::::::::::::::::::::::::::::: " << std::endl;
+#endif
+ gint startline = layout->paragraphIndex(start_selection);
+ gint endline = layout->paragraphIndex(end_selection);
+ if (start_selection == end_selection) {
+ this->_outer = true;
+ gint counter = 0;
+ for (auto child : tc->text->childList(false)) {
+ SPItem *item = dynamic_cast<SPItem *>(child);
+ if (item && counter == startline) {
+ this->_sub_active_item = item;
+ int origin_selection = layout->iteratorToCharIndex(start_selection);
+ Inkscape::Text::Layout::iterator next = layout->charIndexToIterator(origin_selection + 1);
+ Inkscape::Text::Layout::iterator prev = layout->charIndexToIterator(origin_selection - 1);
+ //TODO: find a better way to init
+ _updating = true;
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ _query_cursor = query;
+ Inkscape::Text::Layout::iterator start_line = tc->text_sel_start;
+ start_line.thisStartOfLine();
+ if (tc->text_sel_start == start_line) {
+ tc->text_sel_start = next;
+ } else {
+ tc->text_sel_start = prev;
+ }
+ _cusor_numbers = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &_query_cursor, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ tc->text_sel_start = start_selection;
+ _updating = false;
+ break;
+ }
+ ++counter;
+ }
+ selection_changed(nullptr);
+ } else if ((start_selection == start && end_selection == end)) {
+ // full subselection
+ _cusor_numbers = 0;
+ this->_outer = true;
+ selection_changed(nullptr);
+ } else {
+ _cusor_numbers = 0;
+ this->_outer = false;
+ wrap_start = tc->text_sel_start;
+ wrap_end = tc->text_sel_end;
+ if (tc->text_sel_start > tc->text_sel_end) {
+ wrap_start.thisEndOfLine();
+ wrap_end.thisStartOfLine();
+ } else {
+ wrap_start.thisStartOfLine();
+ wrap_end.thisEndOfLine();
+ }
+ selection_changed(nullptr);
+ }
+ }
+ }
+#ifdef DEBUG_TEXT
+ std::cout << "subselection_changed: exit " << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << std::endl;
+#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 :
diff --git a/src/ui/toolbar/text-toolbar.h b/src/ui/toolbar/text-toolbar.h
new file mode 100644
index 0000000..6108dd6
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.h
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_TEXT_TOOLBAR_H
+#define SEEN_TEXT_TOOLBAR_H
+
+/**
+ * @file
+ * Text aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "object/sp-item.h"
+#include "object/sp-object.h"
+#include "toolbar.h"
+#include "text-editing.h"
+#include "style.h"
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/separatortoolitem.h>
+#include <sigc++/connection.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ComboBoxText;
+class ToggleToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class ComboBoxEntryToolItem;
+class ComboToolItem;
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class TextToolbar : public Toolbar {
+private:
+ bool _freeze;
+ bool _text_style_from_prefs;
+ UI::Widget::UnitTracker *_tracker;
+ UI::Widget::UnitTracker *_tracker_fs;
+
+ UI::Widget::ComboBoxEntryToolItem *_font_family_item;
+ UI::Widget::ComboBoxEntryToolItem *_font_size_item;
+ UI::Widget::ComboToolItem *_font_size_units_item;
+ UI::Widget::ComboBoxEntryToolItem *_font_style_item;
+ UI::Widget::ComboToolItem *_line_height_units_item;
+ UI::Widget::SpinButtonToolItem *_line_height_item;
+ Gtk::ToggleToolButton *_superscript_item;
+ Gtk::ToggleToolButton *_subscript_item;
+
+ UI::Widget::ComboToolItem *_align_item;
+ UI::Widget::ComboToolItem *_writing_mode_item;
+ UI::Widget::ComboToolItem *_orientation_item;
+ UI::Widget::ComboToolItem *_direction_item;
+
+ UI::Widget::SpinButtonToolItem *_word_spacing_item;
+ UI::Widget::SpinButtonToolItem *_letter_spacing_item;
+ UI::Widget::SpinButtonToolItem *_dx_item;
+ UI::Widget::SpinButtonToolItem *_dy_item;
+ UI::Widget::SpinButtonToolItem *_rotation_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _line_height_adj;
+ Glib::RefPtr<Gtk::Adjustment> _word_spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _letter_spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _dx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _dy_adj;
+ Glib::RefPtr<Gtk::Adjustment> _rotation_adj;
+ bool _outer;
+ SPItem *_sub_active_item;
+ int _lineheight_unit;
+ Inkscape::Text::Layout::iterator wrap_start;
+ Inkscape::Text::Layout::iterator wrap_end;
+ bool _updating;
+ int _cusor_numbers;
+ SPStyle _query_cursor;
+ double selection_fontsize;
+ sigc::connection c_selection_changed;
+ sigc::connection c_selection_modified;
+ sigc::connection c_selection_modified_select_tool;
+ sigc::connection c_subselection_changed;
+ void text_outer_set_style(SPCSSAttr *css);
+ void fontfamily_value_changed();
+ void fontsize_value_changed();
+ void subselection_wrap_toggle(bool start);
+ void fontstyle_value_changed();
+ void script_changed(Gtk::ToggleToolButton *btn);
+ void align_mode_changed(int mode);
+ void writing_mode_changed(int mode);
+ void orientation_changed(int mode);
+ void direction_changed(int mode);
+ void lineheight_value_changed();
+ void lineheight_unit_changed(int not_used);
+ void wordspacing_value_changed();
+ void letterspacing_value_changed();
+ void dx_value_changed();
+ void dy_value_changed();
+ void prepare_inner();
+ void focus_text();
+ void rotation_value_changed();
+ void fontsize_unit_changed(int not_used);
+ void selection_changed(Inkscape::Selection *selection);
+ void selection_modified(Inkscape::Selection *selection, guint flags);
+ void selection_modified_select_tool(Inkscape::Selection *selection, guint flags);
+ void subselection_changed(gpointer texttool);
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void set_sizes(int unit);
+ Inkscape::XML::Node *unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *before);
+
+ protected:
+ TextToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+}
+}
+}
+
+#endif /* !SEEN_TEXT_TOOLBAR_H */
diff --git a/src/ui/toolbar/toolbar.cpp b/src/ui/toolbar/toolbar.cpp
new file mode 100644
index 0000000..445f5b7
--- /dev/null
+++ b/src/ui/toolbar/toolbar.cpp
@@ -0,0 +1,102 @@
+// 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 "toolbar.h"
+
+#include <gtkmm/label.h>
+#include <gtkmm/separatortoolitem.h>
+#include <gtkmm/toggletoolbutton.h>
+
+#include "desktop.h"
+
+#include "helper/action.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+Gtk::ToolItem *
+Toolbar::add_label(const Glib::ustring &label_text)
+{
+ auto ti = Gtk::manage(new Gtk::ToolItem());
+
+ // For now, we always enable mnemonic
+ auto label = Gtk::manage(new Gtk::Label(label_text, true));
+
+ ti->add(*label);
+ add(*ti);
+
+ return ti;
+}
+
+/**
+ * \brief Add a toggle toolbutton to the toolbar
+ *
+ * \param[in] label_text The text to display in the toolbar
+ * \param[in] tooltip_text The tooltip text for the toolitem
+ *
+ * \returns The toggle button
+ */
+Gtk::ToggleToolButton *
+Toolbar::add_toggle_button(const Glib::ustring &label_text,
+ const Glib::ustring &tooltip_text)
+{
+ auto btn = Gtk::manage(new Gtk::ToggleToolButton(label_text));
+ btn->set_tooltip_text(tooltip_text);
+ add(*btn);
+ return btn;
+}
+
+/**
+ * \brief Add a toolbutton that performs a given verb
+ *
+ * \param[in] verb_code The code for the verb (e.g., SP_VERB_EDIT_SELECT_ALL)
+ *
+ * \returns a pointer to the toolbutton
+ */
+Gtk::ToolButton *
+Toolbar::add_toolbutton_for_verb(unsigned int verb_code)
+{
+ auto context = Inkscape::ActionContext(_desktop);
+ auto button = SPAction::create_toolbutton_for_verb(verb_code, context);
+ add(*button);
+ return button;
+}
+
+/**
+ * \brief Add a separator line to the toolbar
+ *
+ * \details This is just a convenience wrapper for the
+ * standard GtkMM functionality
+ */
+void
+Toolbar::add_separator()
+{
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+}
+
+GtkWidget *
+Toolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = Gtk::manage(new Toolbar(desktop));
+ return GTK_WIDGET(toolbar->gobj());
+}
+}
+}
+}
+/*
+ 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/toolbar/toolbar.h b/src/ui/toolbar/toolbar.h
new file mode 100644
index 0000000..18c0510
--- /dev/null
+++ b/src/ui/toolbar/toolbar.h
@@ -0,0 +1,67 @@
+// 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.
+ */
+#ifndef SEEN_TOOLBAR_H
+#define SEEN_TOOLBAR_H
+
+#include <gtkmm/toolbar.h>
+
+class SPDesktop;
+
+namespace Gtk {
+ class Label;
+ class ToggleToolButton;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+/**
+ * \brief An abstract definition for a toolbar within Inkscape
+ *
+ * \detail This is basically the same as a Gtk::Toolbar but contains a
+ * few convenience functions. All toolbars must define a "create"
+ * function that adds all the required tool-items and returns the
+ * toolbar as a GtkWidget
+ */
+class Toolbar : public Gtk::Toolbar {
+protected:
+ SPDesktop *_desktop;
+
+ /**
+ * \brief A default constructor that just assigns the desktop
+ */
+ Toolbar(SPDesktop *desktop)
+ : _desktop(desktop)
+ {}
+
+ Gtk::ToolItem * add_label(const Glib::ustring &label_text);
+ Gtk::ToggleToolButton * add_toggle_button(const Glib::ustring &label_text,
+ const Glib::ustring &tooltip_text);
+ Gtk::ToolButton * add_toolbutton_for_verb(unsigned int verb_code);
+ void add_separator();
+
+protected:
+ static GtkWidget * create(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/toolbar/tweak-toolbar.cpp b/src/ui/toolbar/tweak-toolbar.cpp
new file mode 100644
index 0000000..dc5352c
--- /dev/null
+++ b/src/ui/toolbar/tweak-toolbar.cpp
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Tweak aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tweak-toolbar.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/radiotoolbutton.h>
+#include <gtkmm/separatortoolitem.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/tweak-tool.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+TweakToolbar::TweakToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* Width */
+ {
+ std::vector<Glib::ustring> labels = {_("(pinch tweak)"), "", "", "", _("(default)"), "", "", "", "", _("(broad tweak)")};
+ std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100};
+
+ auto width_val = prefs->getDouble("/tools/tweak/width", 15);
+ _width_adj = Gtk::Adjustment::create(width_val * 100, 1, 100, 1.0, 10.0);
+ _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-width", _("Width:"), _width_adj, 0.01, 0));
+ _width_item->set_tooltip_text(_("The width of the tweak area (relative to the visible canvas area)"));
+ _width_item->set_custom_numeric_menu_data(values, labels);
+ _width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::width_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*_width_item);
+ _width_item->set_sensitive(true);
+ }
+
+ // Force
+ {
+ std::vector<Glib::ustring> labels = {_("(minimum force)"), "", "", _("(default)"), "", "", "", _("(maximum force)")};
+ std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100};
+ auto force_val = prefs->getDouble("/tools/tweak/force", 20);
+ _force_adj = Gtk::Adjustment::create(force_val * 100, 1, 100, 1.0, 10.0);
+ _force_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-force", _("Force:"), _force_adj, 0.01, 0));
+ _force_item->set_tooltip_text(_("The force of the tweak action"));
+ _force_item->set_custom_numeric_menu_data(values, labels);
+ _force_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _force_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::force_value_changed));
+ // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT );
+ add(*_force_item);
+ _force_item->set_sensitive(true);
+ }
+
+ /* Use Pressure button */
+ {
+ _pressure_item = add_toggle_button(_("Pressure"),
+ _("Use the pressure of the input device to alter the force of tweak action"));
+ _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure"));
+ _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::pressure_state_changed));
+ _pressure_item->set_active(prefs->getBool("/tools/tweak/usepressure", true));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Mode */
+ {
+ add_label(_("Mode:"));
+ Gtk::RadioToolButton::Group mode_group;
+
+ auto mode_move_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move mode")));
+ mode_move_btn->set_tooltip_text(_("Move objects in any direction"));
+ mode_move_btn->set_icon_name(INKSCAPE_ICON("object-tweak-push"));
+ _mode_buttons.push_back(mode_move_btn);
+
+ auto mode_inout_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move in/out mode")));
+ mode_inout_btn->set_tooltip_text(_("Move objects towards cursor; with Shift from cursor"));
+ mode_inout_btn->set_icon_name(INKSCAPE_ICON("object-tweak-attract"));
+ _mode_buttons.push_back(mode_inout_btn);
+
+ auto mode_jitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move jitter mode")));
+ mode_jitter_btn->set_tooltip_text(_("Move objects in random directions"));
+ mode_jitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-randomize"));
+ _mode_buttons.push_back(mode_jitter_btn);
+
+ auto mode_scale_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Scale mode")));
+ mode_scale_btn->set_tooltip_text(_("Shrink objects, with Shift enlarge"));
+ mode_scale_btn->set_icon_name(INKSCAPE_ICON("object-tweak-shrink"));
+ _mode_buttons.push_back(mode_scale_btn);
+
+ auto mode_rotate_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Rotate mode")));
+ mode_rotate_btn->set_tooltip_text(_("Rotate objects, with Shift counterclockwise"));
+ mode_rotate_btn->set_icon_name(INKSCAPE_ICON("object-tweak-rotate"));
+ _mode_buttons.push_back(mode_rotate_btn);
+
+ auto mode_dupdel_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Duplicate/delete mode")));
+ mode_dupdel_btn->set_tooltip_text(_("Duplicate objects, with Shift delete"));
+ mode_dupdel_btn->set_icon_name(INKSCAPE_ICON("object-tweak-duplicate"));
+ _mode_buttons.push_back(mode_dupdel_btn);
+
+ auto mode_push_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Push mode")));
+ mode_push_btn->set_tooltip_text(_("Push parts of paths in any direction"));
+ mode_push_btn->set_icon_name(INKSCAPE_ICON("path-tweak-push"));
+ _mode_buttons.push_back(mode_push_btn);
+
+ auto mode_shrinkgrow_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Shrink/grow mode")));
+ mode_shrinkgrow_btn->set_tooltip_text(_("Shrink (inset) parts of paths; with Shift grow (outset)"));
+ mode_shrinkgrow_btn->set_icon_name(INKSCAPE_ICON("path-tweak-shrink"));
+ _mode_buttons.push_back(mode_shrinkgrow_btn);
+
+ auto mode_attrep_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Attract/repel mode")));
+ mode_attrep_btn->set_tooltip_text(_("Attract parts of paths towards cursor; with Shift from cursor"));
+ mode_attrep_btn->set_icon_name(INKSCAPE_ICON("path-tweak-attract"));
+ _mode_buttons.push_back(mode_attrep_btn);
+
+ auto mode_roughen_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Roughen mode")));
+ mode_roughen_btn->set_tooltip_text(_("Roughen parts of paths"));
+ mode_roughen_btn->set_icon_name(INKSCAPE_ICON("path-tweak-roughen"));
+ _mode_buttons.push_back(mode_roughen_btn);
+
+ auto mode_colpaint_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color paint mode")));
+ mode_colpaint_btn->set_tooltip_text(_("Paint the tool's color upon selected objects"));
+ mode_colpaint_btn->set_icon_name(INKSCAPE_ICON("object-tweak-paint"));
+ _mode_buttons.push_back(mode_colpaint_btn);
+
+ auto mode_coljitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color jitter mode")));
+ mode_coljitter_btn->set_tooltip_text(_("Jitter the colors of selected objects"));
+ mode_coljitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-jitter-color"));
+ _mode_buttons.push_back(mode_coljitter_btn);
+
+ auto mode_blur_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Blur mode")));
+ mode_blur_btn->set_tooltip_text(_("Blur selected objects more; with Shift, blur less"));
+ mode_blur_btn->set_icon_name(INKSCAPE_ICON("object-tweak-blur"));
+ _mode_buttons.push_back(mode_blur_btn);
+
+ int btn_idx = 0;
+
+ for (auto btn : _mode_buttons) {
+ btn->set_sensitive();
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &TweakToolbar::mode_changed), btn_idx++));
+ }
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ guint mode = prefs->getInt("/tools/tweak/mode", 0);
+
+ /* Fidelity */
+ {
+ std::vector<Glib::ustring> labels = {_("(rough, simplified)"), "", "", _("(default)"), "", "", _("(fine, but many nodes)")};
+ std::vector<double> values = { 10, 25, 35, 50, 60, 80, 100};
+
+ auto fidelity_val = prefs->getDouble("/tools/tweak/fidelity", 50);
+ _fidelity_adj = Gtk::Adjustment::create(fidelity_val * 100, 1, 100, 1.0, 10.0);
+ _fidelity_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-fidelity", _("Fidelity:"), _fidelity_adj, 0.01, 0));
+ _fidelity_item->set_tooltip_text(_("Low fidelity simplifies paths; high fidelity preserves path features but may generate a lot of new nodes"));
+ _fidelity_item->set_custom_numeric_menu_data(values, labels);
+ _fidelity_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas)));
+ _fidelity_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::fidelity_value_changed));
+ add(*_fidelity_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ _channels_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Channels:")));
+ _channels_label->set_use_markup(true);
+ add(*_channels_label);
+ }
+
+ {
+ //TRANSLATORS: "H" here stands for hue
+ _doh_item = add_toggle_button(C_("Hue", "H"),
+ _("In color mode, act on object's hue"));
+ _doh_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doh));
+ _doh_item->set_active(prefs->getBool("/tools/tweak/doh", true));
+ }
+ {
+ //TRANSLATORS: "S" here stands for saturation
+ _dos_item = add_toggle_button(C_("Saturation", "S"),
+ _("In color mode, act on object's saturation"));
+ _dos_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dos));
+ _dos_item->set_active(prefs->getBool("/tools/tweak/dos", true));
+ }
+ {
+ //TRANSLATORS: "S" here stands for saturation
+ _dol_item = add_toggle_button(C_("Lightness", "L"),
+ _("In color mode, act on object's lightness"));
+ _dol_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dol));
+ _dol_item->set_active(prefs->getBool("/tools/tweak/dol", true));
+ }
+ {
+ //TRANSLATORS: "O" here stands for opacity
+ _doo_item = add_toggle_button(C_("Opacity", "O"),
+ _("In color mode, act on object's opacity"));
+ _doo_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doo));
+ _doo_item->set_active(prefs->getBool("/tools/tweak/doo", true));
+ }
+
+ _mode_buttons[mode]->set_active();
+ show_all();
+
+ // Elements must be hidden after show_all() is called
+ if (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT || mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER) {
+ _fidelity_item->set_visible(false);
+ } else {
+ _channels_label->set_visible(false);
+ _doh_item->set_visible(false);
+ _dos_item->set_visible(false);
+ _dol_item->set_visible(false);
+ _doo_item->set_visible(false);
+ }
+}
+
+void
+TweakToolbar::set_mode(int mode)
+{
+ _mode_buttons[mode]->set_active();
+}
+
+GtkWidget *
+TweakToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new TweakToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+TweakToolbar::width_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/tweak/width",
+ _width_adj->get_value() * 0.01 );
+}
+
+void
+TweakToolbar::force_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/tweak/force",
+ _force_adj->get_value() * 0.01 );
+}
+
+void
+TweakToolbar::mode_changed(int mode)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/tweak/mode", mode);
+
+ bool flag = ((mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT) ||
+ (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER));
+
+ _doh_item->set_visible(flag);
+ _dos_item->set_visible(flag);
+ _dol_item->set_visible(flag);
+ _doo_item->set_visible(flag);
+ _channels_label->set_visible(flag);
+
+ if (_fidelity_item) {
+ _fidelity_item->set_visible(!flag);
+ }
+}
+
+void
+TweakToolbar::fidelity_value_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble( "/tools/tweak/fidelity",
+ _fidelity_adj->get_value() * 0.01 );
+}
+
+void
+TweakToolbar::pressure_state_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/tweak/usepressure", _pressure_item->get_active());
+}
+
+void
+TweakToolbar::toggle_doh() {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/tweak/doh", _doh_item->get_active());
+}
+
+void
+TweakToolbar::toggle_dos() {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/tweak/dos", _dos_item->get_active());
+}
+
+void
+TweakToolbar::toggle_dol() {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/tweak/dol", _dol_item->get_active());
+}
+
+void
+TweakToolbar::toggle_doo() {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/tweak/doo", _doo_item->get_active());
+}
+
+}
+}
+}
+
+/*
+ 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/toolbar/tweak-toolbar.h b/src/ui/toolbar/tweak-toolbar.h
new file mode 100644
index 0000000..cd1c7d0
--- /dev/null
+++ b/src/ui/toolbar/tweak-toolbar.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_TWEAK_TOOLBAR_H
+#define SEEN_TWEAK_TOOLBAR_H
+
+/**
+ * @file
+ * Tweak aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+class SPDesktop;
+
+namespace Gtk {
+class RadioToolButton;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class TweakToolbar : public Toolbar {
+private:
+ UI::Widget::SpinButtonToolItem *_width_item;
+ UI::Widget::SpinButtonToolItem *_force_item;
+ UI::Widget::SpinButtonToolItem *_fidelity_item;
+
+ Gtk::ToggleToolButton *_pressure_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _force_adj;
+ Glib::RefPtr<Gtk::Adjustment> _fidelity_adj;
+
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+
+ UI::Widget::LabelToolItem *_channels_label;
+ Gtk::ToggleToolButton *_doh_item;
+ Gtk::ToggleToolButton *_dos_item;
+ Gtk::ToggleToolButton *_dol_item;
+ Gtk::ToggleToolButton *_doo_item;
+
+ void width_value_changed();
+ void force_value_changed();
+ void mode_changed(int mode);
+ void fidelity_value_changed();
+ void pressure_state_changed();
+ void toggle_doh();
+ void toggle_dos();
+ void toggle_dol();
+ void toggle_doo();
+
+protected:
+ TweakToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ void set_mode(int mode);
+};
+}
+}
+}
+
+#endif /* !SEEN_SELECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/zoom-toolbar.cpp b/src/ui/toolbar/zoom-toolbar.cpp
new file mode 100644
index 0000000..3b4d3d6
--- /dev/null
+++ b/src/ui/toolbar/zoom-toolbar.cpp
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Zoom aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "zoom-toolbar.h"
+
+#include "desktop.h"
+#include "verbs.h"
+
+#include "helper/action.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ZoomToolbar::ZoomToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ add_toolbutton_for_verb(SP_VERB_ZOOM_IN);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_OUT);
+
+ add_separator();
+
+ add_toolbutton_for_verb(SP_VERB_ZOOM_1_1);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_1_2);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_2_1);
+
+ add_separator();
+
+ add_toolbutton_for_verb(SP_VERB_ZOOM_SELECTION);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_DRAWING);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_PAGE);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_PAGE_WIDTH);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_CENTER_PAGE);
+
+ add_separator();
+
+ add_toolbutton_for_verb(SP_VERB_ZOOM_PREV);
+ add_toolbutton_for_verb(SP_VERB_ZOOM_NEXT);
+
+ show_all();
+}
+
+GtkWidget *
+ZoomToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = Gtk::manage(new ZoomToolbar(desktop));
+ return GTK_WIDGET(toolbar->gobj());
+}
+}
+}
+}
+
+/*
+ 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/toolbar/zoom-toolbar.h b/src/ui/toolbar/zoom-toolbar.h
new file mode 100644
index 0000000..b5d34de
--- /dev/null
+++ b/src/ui/toolbar/zoom-toolbar.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_ZOOM_TOOLBAR_H
+#define SEEN_ZOOM_TOOLBAR_H
+
+/**
+ * @file
+ * Zoom aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+/**
+ * \brief A toolbar for controlling the zoom
+ */
+class ZoomToolbar : public Toolbar {
+protected:
+ ZoomToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+}
+}
+}
+
+#endif /* !SEEN_ZOOM_TOOLBAR_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/tools-switch.cpp b/src/ui/tools-switch.cpp
new file mode 100644
index 0000000..46695cf
--- /dev/null
+++ b/src/ui/tools-switch.cpp
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Utility functions for switching tools (= contexts)
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Josh Andler <scislac@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2003-2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h> // prevents deprecation warnings
+
+#include "inkscape.h"
+#include "desktop.h"
+
+#include <glibmm/i18n.h>
+
+#include "ui/tools-switch.h"
+
+#include "object/sp-rect.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-offset.h"
+#include "object/sp-path.h"
+#include "object/sp-star.h"
+#include "object/sp-spiral.h"
+#include "object/sp-text.h"
+
+// TODO: How many of these are actually needed?
+#include "ui/tools/arc-tool.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/tools/calligraphic-tool.h"
+#include "ui/tools/connector-tool.h"
+#include "ui/tools/dropper-tool.h"
+#include "ui/tools/eraser-tool.h"
+#include "ui/tools/flood-tool.h"
+#include "ui/tools/gradient-tool.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/tools/measure-tool.h"
+#include "ui/tools/mesh-tool.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/pencil-tool.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/spiral-tool.h"
+#include "ui/tools/spray-tool.h"
+#include "ui/tools/text-tool.h"
+#include "ui/tools/tweak-tool.h"
+#include "ui/tools/zoom-tool.h"
+
+#include "message-context.h"
+
+using Inkscape::UI::Tools::ToolBase;
+
+static char const *const tool_names[] = {
+ nullptr,
+ "/tools/select",
+ "/tools/nodes",
+ "/tools/tweak",
+ "/tools/spray",
+ "/tools/shapes/rect",
+ "/tools/shapes/3dbox",
+ "/tools/shapes/arc",
+ "/tools/shapes/star",
+ "/tools/shapes/spiral",
+ "/tools/freehand/pencil",
+ "/tools/freehand/pen",
+ "/tools/calligraphic",
+ "/tools/text",
+ "/tools/gradient",
+ "/tools/mesh",
+ "/tools/zoom",
+ "/tools/measure",
+ "/tools/dropper",
+ "/tools/connector",
+ "/tools/paintbucket",
+ "/tools/eraser",
+ "/tools/lpetool",
+ nullptr
+};
+
+// TODO: HEY! these belong to the tools themselves!
+static char const *const tool_msg[] = {
+ nullptr,
+ N_("<b>Click</b> to Select and Transform objects, <b>Drag</b> to select many objects."),
+ N_("Modify selected path points (nodes) directly."),
+ N_("To tweak a path by pushing, select it and drag over it."),
+ N_("<b>Drag</b>, <b>click</b> or <b>click and scroll</b> to spray the selected objects."),
+ N_("<b>Drag</b> to create a rectangle. <b>Drag controls</b> to round corners and resize. <b>Click</b> to select."),
+ N_("<b>Drag</b> to create a 3D box. <b>Drag controls</b> to resize in perspective. <b>Click</b> to select (with <b>Ctrl+Alt</b> for single faces)."),
+ N_("<b>Drag</b> to create an ellipse. <b>Drag controls</b> to make an arc or segment. <b>Click</b> to select."),
+ N_("<b>Drag</b> to create a star. <b>Drag controls</b> to edit the star shape. <b>Click</b> to select."),
+ N_("<b>Drag</b> to create a spiral. <b>Drag controls</b> to edit the spiral shape. <b>Click</b> to select."),
+ N_("<b>Drag</b> to create a freehand line. <b>Shift</b> appends to selected path, <b>Alt</b> activates sketch mode."),
+ N_("<b>Click</b> or <b>click and drag</b> to start a path; with <b>Shift</b> to append to selected path. <b>Ctrl+click</b> to create single dots (straight line modes only)."),
+ N_("<b>Drag</b> to draw a calligraphic stroke; with <b>Ctrl</b> to track a guide path. <b>Arrow keys</b> adjust width (left/right) and angle (up/down)."),
+ N_("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type."),
+ N_("<b>Drag</b> or <b>double click</b> to create a gradient on selected objects, <b>drag handles</b> to adjust gradients."),
+ N_("<b>Drag</b> or <b>double click</b> to create a mesh on selected objects, <b>drag handles</b> to adjust meshes."),
+ N_("<b>Click</b> or <b>drag around an area</b> to zoom in, <b>Shift+click</b> to zoom out."),
+ N_("<b>Drag</b> to measure the dimensions of objects."),
+ N_("<b>Click</b> to set fill, <b>Shift+click</b> to set stroke; <b>drag</b> to average color in area; with <b>Alt</b> to pick inverse color; <b>Ctrl+C</b> to copy the color under mouse to clipboard"),
+ N_("<b>Click and drag</b> between shapes to create a connector."),
+ N_("<b>Click</b> to paint a bounded area, <b>Shift+click</b> to union the new fill with the current selection, <b>Ctrl+click</b> to change the clicked object's fill and stroke to the current setting."),
+ N_("<b>Drag</b> to erase."),
+ N_("Choose a subtool from the toolbar"),
+};
+
+static int
+tools_prefpath2num(char const *id)
+{
+ int i = 1;
+ while (tool_names[i]) {
+ if (strcmp(tool_names[i], id) == 0)
+ return i;
+ else i++;
+ }
+ g_assert( 0 == TOOLS_INVALID );
+ return 0; //nothing found
+}
+
+int
+tools_isactive(SPDesktop *dt, unsigned num)
+{
+ g_assert( num < G_N_ELEMENTS(tool_names) );
+ if (dynamic_cast<ToolBase *>(dt->event_context)) {
+ return dt->event_context->pref_observer->observed_path == tool_names[num];
+ } else {
+ return FALSE;
+ }
+}
+
+int
+tools_active(SPDesktop *dt)
+{
+ return tools_prefpath2num(dt->event_context->pref_observer->observed_path.data());
+}
+
+void
+tools_switch(SPDesktop *dt, int num)
+{
+ dt->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, gettext( tool_msg[num] ) );
+ if (dt) {
+ // This event may change the above message
+ dt->_tool_changed.emit(num);
+ }
+
+ dt->setEventContext(tool_names[num]);
+ /* fixme: This is really ugly hack. We should bind and unbind class methods */
+ /* First 4 tools use guides, first is undefined but we don't care */
+ dt->activate_guides(num < 5);
+ INKSCAPE.eventcontext_set(dt->getEventContext());
+}
+
+void tools_switch_by_item(SPDesktop *dt, SPItem *item, Geom::Point const p)
+{
+ if (dynamic_cast<SPRect *>(item)) {
+ tools_switch(dt, TOOLS_SHAPES_RECT);
+ } else if (dynamic_cast<SPBox3D *>(item)) {
+ tools_switch(dt, TOOLS_SHAPES_3DBOX);
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ tools_switch(dt, TOOLS_SHAPES_ARC);
+ } else if (dynamic_cast<SPStar *>(item)) {
+ tools_switch(dt, TOOLS_SHAPES_STAR);
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ tools_switch(dt, TOOLS_SHAPES_SPIRAL);
+ } else if (dynamic_cast<SPPath *>(item)) {
+ if (Inkscape::UI::Tools::cc_item_is_connector(item)) {
+ tools_switch(dt, TOOLS_CONNECTOR);
+ }
+ else {
+ tools_switch(dt, TOOLS_NODES);
+ }
+ } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ tools_switch(dt, TOOLS_TEXT);
+ sp_text_context_place_cursor_at (SP_TEXT_CONTEXT(dt->event_context), item, p);
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ tools_switch(dt, 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/tools-switch.h b/src/ui/tools-switch.h
new file mode 100644
index 0000000..ad2a731
--- /dev/null
+++ b/src/ui/tools-switch.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Utility functions for switching tools (= contexts)
+ *
+ * Authors:
+ * bulia byak <bulia@dr.com>
+ *
+ * Copyright (C) 2003 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_TOOLS_SWITCH_H
+#define SEEN_TOOLS_SWITCH_H
+
+class SPDesktop;
+class SPItem;
+namespace Geom {
+class Point;
+}
+
+
+enum {
+ TOOLS_INVALID,
+ TOOLS_SELECT,
+ TOOLS_NODES,
+ TOOLS_TWEAK,
+ TOOLS_SPRAY,
+ TOOLS_SHAPES_RECT,
+ TOOLS_SHAPES_3DBOX,
+ TOOLS_SHAPES_ARC,
+ TOOLS_SHAPES_STAR,
+ TOOLS_SHAPES_SPIRAL,
+ TOOLS_FREEHAND_PENCIL,
+ TOOLS_FREEHAND_PEN,
+ TOOLS_CALLIGRAPHIC,
+ TOOLS_TEXT,
+ TOOLS_GRADIENT,
+ TOOLS_MESH,
+ TOOLS_ZOOM,
+ TOOLS_MEASURE,
+ TOOLS_DROPPER,
+ TOOLS_CONNECTOR,
+ TOOLS_PAINTBUCKET,
+ TOOLS_ERASER,
+ TOOLS_LPETOOL
+};
+
+int tools_isactive(SPDesktop *dt, unsigned num);
+int tools_active(SPDesktop *dt);
+void tools_switch(SPDesktop *dt, int num);
+void tools_switch_by_item (SPDesktop *dt, SPItem *item, Geom::Point const p);
+
+#endif // !SEEN_TOOLS_SWITCH_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp
new file mode 100644
index 0000000..8356817
--- /dev/null
+++ b/src/ui/tools/arc-tool.cpp
@@ -0,0 +1,488 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Ellipse drawing context.
+ */
+/* Authors:
+ * Mitsuru Oka
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <johan@shouraizou.nl>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2000-2006 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <glibmm/i18n.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+#include "preferences.h"
+#include "selection.h"
+#include "snap.h"
+#include "verbs.h"
+
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-item.h"
+
+#include "include/macros.h"
+
+#include "object/sp-ellipse.h"
+#include "object/sp-namedview.h"
+
+#include "ui/pixmaps/cursor-ellipse.xpm"
+
+#include "ui/tools/arc-tool.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/tool-base.h"
+
+#include "xml/repr.h"
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& ArcTool::getPrefsPath() {
+ return ArcTool::prefsPath;
+}
+
+const std::string ArcTool::prefsPath = "/tools/shapes/arc";
+
+
+ArcTool::ArcTool()
+ : ToolBase(cursor_ellipse_xpm)
+ , arc(nullptr)
+{
+}
+
+void ArcTool::finish() {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ ToolBase::finish();
+}
+
+ArcTool::~ArcTool() {
+ this->enableGrDrag(false);
+
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->arc) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ */
+void ArcTool::selection_changed(Inkscape::Selection* selection) {
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+}
+
+void ArcTool::setup() {
+ ToolBase::setup();
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = selection->connectChanged(
+ sigc::mem_fun(this, &ArcTool::selection_changed)
+ );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+bool ArcTool::item_handler(SPItem* item, GdkEvent* event) {
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ Inkscape::setup_for_drag_start(desktop, this, event);
+ }
+ break;
+ // motion and release are always on root (why?)
+ default:
+ break;
+ }
+
+ return ToolBase::item_handler(item, event);
+}
+
+bool ArcTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ bool handled = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = true;
+
+ this->center = Inkscape::setup_for_drag_start(desktop, this, event);
+
+ /* Snap center */
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK,
+ nullptr, event->button.time);
+ handled = true;
+ m.unSetup();
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ this->drag(motion_dt, event->motion.state);
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ handled = true;
+ } else if (!this->sp_event_context_knot_mouseover()){
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the arc
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+ handled = true;
+ }
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+ if (!dragging) {
+ sp_event_show_modifier_tip(this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: make circle or integer-ratio ellipse, snap arc/segment angle"),
+ _("<b>Shift</b>: draw around the starting point"),
+ nullptr);
+ }
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("arc-rx");
+ handled = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (dragging) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ handled = true;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the arc
+ this->finishItem();
+ }
+ // do not return true, so that space would work switching to selector
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ handled = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (event->key.keyval) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!handled) {
+ handled = ToolBase::root_handler(event);
+ }
+
+ return handled;
+}
+
+void ArcTool::drag(Geom::Point pt, guint state) {
+ if (!this->arc) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return;
+ }
+
+ // Create object
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ repr->setAttribute("sodipodi:type", "arc");
+
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/arc", false);
+
+ this->arc = SP_GENERICELLIPSE(desktop->currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ this->arc->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ this->arc->updateRepr();
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ }
+
+ bool ctrl_save = false;
+
+ if ((state & GDK_MOD1_MASK) && (state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) {
+ // if Alt is pressed without Shift in addition to Control, temporarily drop the CONTROL mask
+ // so that the ellipse is not constrained to integer ratios
+ ctrl_save = true;
+ state = state ^ GDK_CONTROL_MASK;
+ }
+
+ Geom::Rect r = Inkscape::snap_rectangular_box(desktop, this->arc, pt, this->center, state);
+
+ if (ctrl_save) {
+ state = state ^ GDK_CONTROL_MASK;
+ }
+
+ Geom::Point dir = r.dimensions() / 2;
+
+ if (state & GDK_MOD1_MASK) {
+ /* With Alt let the ellipse pass through the mouse pointer */
+ Geom::Point c = r.midpoint();
+
+ if (!ctrl_save) {
+ if (fabs(dir[Geom::X]) > 1E-6 && fabs(dir[Geom::Y]) > 1E-6) {
+ Geom::Affine const i2d ( (this->arc)->i2dt_affine() );
+ Geom::Point new_dir = pt * i2d - c;
+ new_dir[Geom::X] *= dir[Geom::Y] / dir[Geom::X];
+ double lambda = new_dir.length() / dir[Geom::Y];
+ r = Geom::Rect (c - lambda*dir, c + lambda*dir);
+ }
+ } else {
+ /* with Alt+Ctrl (without Shift) we generate a perfect circle
+ with diameter click point <--> mouse pointer */
+ double l = dir.length();
+ Geom::Point d (l, l);
+ r = Geom::Rect (c - d, c + d);
+ }
+ }
+
+ this->arc->position_set(
+ r.midpoint()[Geom::X], r.midpoint()[Geom::Y],
+ r.dimensions()[Geom::X] / 2, r.dimensions()[Geom::Y] / 2);
+
+ double rdimx = r.dimensions()[Geom::X];
+ double rdimy = r.dimensions()[Geom::Y];
+
+ Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px");
+ Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px");
+ Glib::ustring xs = rdimx_q.string(desktop->namedview->display_units);
+ Glib::ustring ys = rdimy_q.string(desktop->namedview->display_units);
+
+ if (state & GDK_CONTROL_MASK) {
+ int ratio_x, ratio_y;
+ bool is_golden_ratio = false;
+
+ if (fabs (rdimx) > fabs (rdimy)) {
+ if (fabs(rdimx / rdimy - goldenratio) < 1e-6) {
+ is_golden_ratio = true;
+ }
+
+ ratio_x = (int) rint (rdimx / rdimy);
+ ratio_y = 1;
+ } else {
+ if (fabs(rdimy / rdimx - goldenratio) < 1e-6) {
+ is_golden_ratio = true;
+ }
+
+ ratio_x = 1;
+ ratio_y = (int) rint (rdimy / rdimx);
+ }
+
+ if (!is_golden_ratio) {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Ellipse</b>: %s &#215; %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str(), ratio_x, ratio_y);
+ } else {
+ if (ratio_y == 1) {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Ellipse</b>: %s &#215; %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str());
+ } else {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Ellipse</b>: %s &#215; %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str());
+ }
+ }
+ } else {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Ellipse</b>: %s &#215; %s; with <b>Ctrl</b> to make circle, integer-ratio, or golden-ratio ellipse; with <b>Shift</b> to draw around the starting point"), xs.c_str(), ys.c_str());
+ }
+}
+
+void ArcTool::finishItem() {
+ this->message_context->clear();
+
+ if (this->arc != nullptr) {
+ if (this->arc->rx.computed == 0 || this->arc->ry.computed == 0) {
+ this->cancel(); // Don't allow the creating of zero sized arc, for example when the start and and point snap to the snap grid point
+ return;
+ }
+
+ this->arc->updateRepr();
+ this->arc->doWriteTransform(this->arc->transform, nullptr, true);
+
+ desktop->canvas->endForcedFullRedraws();
+
+ desktop->getSelection()->set(this->arc);
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_ARC, _("Create ellipse"));
+
+ this->arc = nullptr;
+ }
+}
+
+void ArcTool::cancel() {
+ desktop->getSelection()->clear();
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ if (this->arc != nullptr) {
+ this->arc->deleteObject();
+ this->arc = nullptr;
+ }
+
+ this->within_tolerance = false;
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+
+ desktop->canvas->endForcedFullRedraws();
+
+ DocumentUndo::cancel(desktop->getDocument());
+}
+
+}
+}
+}
+
+
+/*
+ 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/tools/arc-tool.h b/src/ui/tools/arc-tool.h
new file mode 100644
index 0000000..6af99e0
--- /dev/null
+++ b/src/ui/tools/arc-tool.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_ARC_CONTEXT_H
+#define SEEN_ARC_CONTEXT_H
+
+/*
+ * Ellipse drawing context
+ *
+ * Authors:
+ * Mitsuru Oka
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2000-2002 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Mitsuru Oka
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+
+class SPItem;
+class SPGenericEllipse;
+
+namespace Inkscape {
+ class Selection;
+}
+
+#define SP_ARC_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ArcTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_ARC_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ArcTool*>(obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ArcTool : public ToolBase {
+public:
+ ArcTool();
+ ~ArcTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPGenericEllipse *arc;
+
+ Geom::Point center;
+
+ sigc::connection sel_changed_connection;
+
+ void selection_changed(Inkscape::Selection* selection);
+
+ void drag(Geom::Point pt, guint state);
+ void finishItem();
+ void cancel();
+};
+
+}
+}
+}
+
+#endif /* !SEEN_ARC_CONTEXT_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/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp
new file mode 100644
index 0000000..4dc866e
--- /dev/null
+++ b/src/ui/tools/box3d-tool.cpp
@@ -0,0 +1,614 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * 3D box drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de>
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000-2005 authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+#include "perspective-line.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "display/sp-canvas-item.h"
+#include "display/sp-canvas.h"
+
+#include "include/macros.h"
+
+#include "ui/pixmaps/cursor-3dbox.xpm"
+
+#include "object/box3d-side.h"
+#include "object/box3d.h"
+#include "object/sp-defs.h"
+#include "object/sp-namedview.h"
+
+#include "ui/shape-editor.h"
+#include "ui/tools/box3d-tool.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& Box3dTool::getPrefsPath() {
+ return Box3dTool::prefsPath;
+}
+
+const std::string Box3dTool::prefsPath = "/tools/shapes/3dbox";
+
+Box3dTool::Box3dTool()
+ : ToolBase(cursor_3dbox_xpm)
+ , _vpdrag(nullptr)
+ , box3d(nullptr)
+ , ctrl_dragged(false)
+ , extruded(false)
+{
+}
+
+void Box3dTool::finish() {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ ToolBase::finish();
+}
+
+
+Box3dTool::~Box3dTool() {
+ this->enableGrDrag(false);
+
+ delete (this->_vpdrag);
+ this->_vpdrag = nullptr;
+
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->box3d) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ */
+void Box3dTool::selection_changed(Inkscape::Selection* selection) {
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+
+ if (selection->perspList().size() == 1) {
+ // selecting a single box changes the current perspective
+ this->desktop->doc()->setCurrentPersp3D(selection->perspList().front());
+ }
+}
+
+/* Create a default perspective in document defs if none is present (which can happen, among other
+ * circumstances, after 'vacuum defs' or when a pre-0.46 file is opened).
+ */
+static void sp_box3d_context_ensure_persp_in_defs(SPDocument *document) {
+ SPDefs *defs = document->getDefs();
+
+ bool has_persp = false;
+ for (auto& child: defs->children) {
+ if (SP_IS_PERSP3D(&child)) {
+ has_persp = true;
+ break;
+ }
+ }
+
+ if (!has_persp) {
+ document->setCurrentPersp3D(persp3d_create_xml_element (document));
+ }
+}
+
+void Box3dTool::setup() {
+ ToolBase::setup();
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = this->desktop->getSelection()->connectChanged(
+ sigc::mem_fun(this, &Box3dTool::selection_changed)
+ );
+
+ this->_vpdrag = new Box3D::VPDrag(this->desktop->getDocument());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+bool Box3dTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1 && !this->space_panning) {
+ Inkscape::setup_for_drag_start(desktop, this, event);
+ //ret = TRUE;
+ }
+ break;
+ // motion and release are always on root (why?)
+ default:
+ break;
+ }
+
+// if (((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler) {
+// ret = ((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler(event_context, item, event);
+// }
+ // CPPIFY: ret is always overwritten...
+ ret = ToolBase::item_handler(item, event);
+
+ return ret;
+}
+
+bool Box3dTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ SPDocument *document = desktop->getDocument();
+ auto const y_dir = desktop->yaxisdir();
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ Persp3D *cur_persp = document->getCurrentPersp3D();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ gint ret = FALSE;
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1 && !this->space_panning) {
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point button_dt(desktop->w2d(button_w));
+
+ // save drag origin
+ this->xp = (gint) button_w[Geom::X];
+ this->yp = (gint) button_w[Geom::Y];
+ this->within_tolerance = true;
+
+ // remember clicked box3d, *not* disregarding groups (since a 3D box is a group), honoring Alt
+ this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, event->button.state & GDK_CONTROL_MASK);
+
+ dragging = true;
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, this->box3d);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ this->center = button_dt;
+
+ this->drag_origin = button_dt;
+ this->drag_ptB = button_dt;
+ this->drag_ptC = button_dt;
+
+ // This can happen after saving when the last remaining perspective was purged and must be recreated.
+ if (!cur_persp) {
+ sp_box3d_context_ensure_persp_in_defs(document);
+ cur_persp = document->getCurrentPersp3D();
+ }
+
+ /* Projective preimages of clicked point under current perspective */
+ this->drag_origin_proj = cur_persp->perspective_impl->tmat.preimage (button_dt, 0, Proj::Z);
+ this->drag_ptB_proj = this->drag_origin_proj;
+ this->drag_ptC_proj = this->drag_origin_proj;
+ this->drag_ptC_proj.normalize();
+ this->drag_ptC_proj[Proj::Z] = 0.25;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ ( GDK_KEY_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK |
+ GDK_BUTTON_PRESS_MASK ),
+ nullptr, event->button.time);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, this->box3d);
+ m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ this->ctrl_dragged = event->motion.state & GDK_CONTROL_MASK;
+
+ if ((event->motion.state & GDK_SHIFT_MASK) && !this->extruded && this->box3d) {
+ // once shift is pressed, set this->extruded
+ this->extruded = true;
+ }
+
+ if (!this->extruded) {
+ this->drag_ptB = motion_dt;
+ this->drag_ptC = motion_dt;
+
+ this->drag_ptB_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, 0, Proj::Z);
+ this->drag_ptC_proj = this->drag_ptB_proj;
+ this->drag_ptC_proj.normalize();
+ this->drag_ptC_proj[Proj::Z] = 0.25;
+ } else {
+ // Without Ctrl, motion of the extruded corner is constrained to the
+ // perspective line from drag_ptB to vanishing point Y.
+ if (!this->ctrl_dragged) {
+ /* snapping */
+ Box3D::PerspectiveLine pline (this->drag_ptB, Proj::Z, document->getCurrentPersp3D());
+ this->drag_ptC = pline.closest_to (motion_dt);
+
+ this->drag_ptB_proj.normalize();
+ this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (this->drag_ptC, this->drag_ptB_proj[Proj::X], Proj::X);
+ } else {
+ this->drag_ptC = motion_dt;
+
+ this->drag_ptB_proj.normalize();
+ this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, this->drag_ptB_proj[Proj::X], Proj::X);
+ }
+
+ m.freeSnapReturnByRef(this->drag_ptC, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ }
+
+ m.unSetup();
+
+ this->drag(event->motion.state);
+
+ ret = TRUE;
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the box
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked box3d if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // prevent the zoom field from activation
+ if (!MOD__CTRL_ONLY(event))
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_bracketright:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ case GDK_KEY_bracketleft:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ case GDK_KEY_parenright:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ case GDK_KEY_parenleft:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ case GDK_KEY_braceright:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ case GDK_KEY_braceleft:
+ persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX,
+ _("Change perspective (angle of PLs)"));
+ ret = true;
+ break;
+
+ /* FOR DEBUGGING PURPOSES
+ case GDK_O:
+ if (MOD__CTRL(event) && MOD__SHIFT(event)) {
+ Box3D::create_canvas_point(persp3d_get_VP(document()->getCurrentPersp3D(), Proj::W).affine(), 7, 0xff00ff00);
+ }
+ ret = true;
+ break;
+ */
+
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_p:
+ case GDK_KEY_P:
+ if (MOD__SHIFT_ONLY(event)) {
+ if (document->getCurrentPersp3D()) {
+ persp3d_print_debugging_info (document->getCurrentPersp3D());
+ }
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("box3d-angle-x");
+ ret = TRUE;
+ }
+ if (MOD__SHIFT_ONLY(event)) {
+ persp3d_toggle_VPs(selection->perspList(), Proj::X);
+ this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically?
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+ if (MOD__SHIFT_ONLY(event)) {
+ persp3d_toggle_VPs(selection->perspList(), Proj::Y);
+ this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically?
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (MOD__SHIFT_ONLY(event)) {
+ persp3d_toggle_VPs(selection->perspList(), Proj::Z);
+ this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically?
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ desktop->getSelection()->clear();
+ //TODO: make dragging escapable by Esc
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the box
+ this->finishItem();
+ }
+ // do not return true, so that space would work switching to selector
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void Box3dTool::drag(guint /*state*/) {
+ if (!this->box3d) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return;
+ }
+
+ // Create object
+ SPBox3D *box3d = SPBox3D::createBox3D((SPItem*)desktop->currentLayer());
+
+ // Set style
+ desktop->applyCurrentOrToolStyle(box3d, "/tools/shapes/3dbox", false);
+
+ this->box3d = box3d;
+
+ // TODO: Incorporate this in box3d-side.cpp!
+ for (int i = 0; i < 6; ++i) {
+ Box3DSide *side = Box3DSide::createBox3DSide(box3d);
+
+ guint desc = Box3D::int_to_face(i);
+
+ Box3D::Axis plane = (Box3D::Axis) (desc & 0x7);
+ plane = (Box3D::is_plane(plane) ? plane : Box3D::orth_plane_or_axis(plane));
+ side->dir1 = Box3D::extract_first_axis_direction(plane);
+ side->dir2 = Box3D::extract_second_axis_direction(plane);
+ side->front_or_rear = (Box3D::FrontOrRear) (desc & 0x8);
+
+ // Set style
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ Glib::ustring descr = "/desktop/";
+ descr += box3d_side_axes_string(side);
+ descr += "/style";
+
+ Glib::ustring cur_style = prefs->getString(descr);
+
+ bool use_current = prefs->getBool("/tools/shapes/3dbox/usecurrent", false);
+
+ if (use_current && !cur_style.empty()) {
+ // use last used style
+ side->setAttribute("style", cur_style);
+ } else {
+ // use default style
+ Glib::ustring tool_path = Glib::ustring::compose("/tools/shapes/3dbox/%1",
+ box3d_side_axes_string(side));
+ desktop->applyCurrentOrToolStyle (side, tool_path, false);
+ }
+
+ side->updateRepr(); // calls box3d_side_write() and updates, e.g., the axes string description
+ }
+
+ box3d_set_z_orders(this->box3d);
+ this->box3d->updateRepr();
+
+ // TODO: It would be nice to show the VPs during dragging, but since there is no selection
+ // at this point (only after finishing the box), we must do this "manually"
+ /* this._vpdrag->updateDraggers(); */
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ }
+
+ g_assert(this->box3d);
+
+ this->box3d->orig_corner0 = this->drag_origin_proj;
+ this->box3d->orig_corner7 = this->drag_ptC_proj;
+
+ box3d_check_for_swapped_coords(this->box3d);
+
+ /* we need to call this from here (instead of from box3d_position_set(), for example)
+ because z-order setting must not interfere with display updates during undo/redo */
+ box3d_set_z_orders (this->box3d);
+
+ box3d_position_set(this->box3d);
+
+ // status text
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, "%s", _("<b>3D Box</b>; with <b>Shift</b> to extrude along the Z axis"));
+}
+
+void Box3dTool::finishItem() {
+ this->message_context->clear();
+ this->ctrl_dragged = false;
+ this->extruded = false;
+
+ if (this->box3d != nullptr) {
+ SPDocument *doc = this->desktop->getDocument();
+
+ if (!doc || !doc->getCurrentPersp3D()) {
+ return;
+ }
+
+ this->box3d->orig_corner0 = this->drag_origin_proj;
+ this->box3d->orig_corner7 = this->drag_ptC_proj;
+
+ this->box3d->updateRepr();
+
+ box3d_relabel_corners(this->box3d);
+
+ desktop->canvas->endForcedFullRedraws();
+
+ desktop->getSelection()->set(this->box3d);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_3DBOX,
+ _("Create 3D box"));
+
+ this->box3d = nullptr;
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/tools/box3d-tool.h b/src/ui/tools/box3d-tool.h
new file mode 100644
index 0000000..1ae63a0
--- /dev/null
+++ b/src/ui/tools/box3d-tool.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_BOX3D_CONTEXT_H__
+#define __SP_BOX3D_CONTEXT_H__
+
+/*
+ * 3D box drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "proj_pt.h"
+#include "vanishing-point.h"
+
+#include "ui/tools/tool-base.h"
+
+class SPItem;
+class SPBox3D;
+
+namespace Box3D {
+ struct VPDrag;
+}
+
+namespace Inkscape {
+ class Selection;
+}
+
+#define SP_BOX3D_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_BOX3D_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::Box3dTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class Box3dTool : public ToolBase {
+public:
+ Box3dTool();
+ ~Box3dTool() override;
+
+ Box3D::VPDrag * _vpdrag;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPBox3D* box3d;
+ Geom::Point center;
+
+ /**
+ * save three corners while dragging:
+ * 1) the starting point (already done by the event_context)
+ * 2) drag_ptB --> the opposite corner of the front face (before pressing shift)
+ * 3) drag_ptC --> the "extruded corner" (which coincides with the mouse pointer location
+ * if we are ctrl-dragging but is constrained to the perspective line from drag_ptC
+ * to the vanishing point Y otherwise)
+ */
+ Geom::Point drag_origin;
+ Geom::Point drag_ptB;
+ Geom::Point drag_ptC;
+
+ Proj::Pt3 drag_origin_proj;
+ Proj::Pt3 drag_ptB_proj;
+ Proj::Pt3 drag_ptC_proj;
+
+ bool ctrl_dragged; /* whether we are ctrl-dragging */
+ bool extruded; /* whether shift-dragging already occurred (i.e. the box is already extruded) */
+
+ sigc::connection sel_changed_connection;
+
+ void selection_changed(Inkscape::Selection* selection);
+
+ void drag(guint state);
+ void finishItem();
+};
+
+}
+}
+}
+
+#endif /* __SP_BOX3D_CONTEXT_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/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp
new file mode 100644
index 0000000..aa5d44d
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.cpp
@@ -0,0 +1,1203 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Handwriting-like drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2005-2007 bulia byak
+ * Copyright (C) 2006 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noDYNA_DRAW_VERBOSE
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include <string>
+#include <cstring>
+#include <numeric>
+
+#include <2geom/pathvector.h>
+#include <2geom/bezier-utils.h>
+#include <2geom/circle.h>
+
+#include "context-fns.h"
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-context.h"
+#include "selection.h"
+#include "splivarot.h"
+#include "verbs.h"
+
+#include "display/cairo-utils.h"
+#include "display/canvas-arena.h"
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+#include "display/sp-canvas.h"
+
+#include "include/macros.h"
+
+#include "livarot/Shape.h"
+
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+
+#include "ui/pixmaps/cursor-calligraphy.xpm"
+
+#include "svg/svg.h"
+
+
+#include "ui/tools/calligraphic-tool.h"
+#include "ui/tools/freehand-base.h"
+
+using Inkscape::DocumentUndo;
+
+#define DDC_RED_RGBA 0xff0000ff
+
+#define TOLERANCE_CALLIGRAPHIC 0.1
+
+#define DYNA_EPSILON 0.5e-6
+#define DYNA_EPSILON_START 0.5e-2
+#define DYNA_VEL_START 1e-5
+
+#define DYNA_MIN_WIDTH 1.0e-6
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void add_cap(SPCurve *curve, Geom::Point const &from, Geom::Point const &to, double rounding);
+
+const std::string& CalligraphicTool::getPrefsPath() {
+ return CalligraphicTool::prefsPath;
+}
+
+
+const std::string CalligraphicTool::prefsPath = "/tools/calligraphic";
+
+CalligraphicTool::CalligraphicTool()
+ : DynamicBase(cursor_calligraphy_xpm)
+ , keep_selected(true)
+ , hatch_spacing(0)
+ , hatch_spacing_step(0)
+ , hatch_item(nullptr)
+ , hatch_livarot_path(nullptr)
+ , hatch_last_nearest(Geom::Point(0,0))
+ , hatch_last_pointer(Geom::Point(0,0))
+ , hatch_escaped(false)
+ , hatch_area(nullptr)
+ , just_started_drawing(false)
+ , trace_bg(false)
+{
+ this->vel_thin = 0.1;
+ this->flatness = 0.9;
+ this->cap_rounding = 0.0;
+ this->abs_width = false;
+}
+
+CalligraphicTool::~CalligraphicTool() {
+ if (this->hatch_area) {
+ sp_canvas_item_destroy(this->hatch_area);
+ this->hatch_area = nullptr;
+ }
+}
+
+void CalligraphicTool::setup() {
+ DynamicBase::setup();
+
+ this->accumulated = new SPCurve();
+ this->currentcurve = new SPCurve();
+
+ this->cal1 = new SPCurve();
+ this->cal2 = new SPCurve();
+
+ this->currentshape = sp_canvas_item_new(this->desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), DDC_RED_RGBA, SP_WIND_RULE_EVENODD);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), this->desktop);
+
+ {
+ /* TODO: have a look at DropperTool::setup where the same is done.. generalize? */
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1));
+
+ SPCurve *c = new SPCurve(path);
+
+ this->hatch_area = sp_canvas_bpath_new(this->desktop->getControls(), c, true);
+
+ c->unref();
+
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->hatch_area), 0x00000000,(SPWindRule)0);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_hide(this->hatch_area);
+ }
+
+ sp_event_context_read(this, "mass");
+ sp_event_context_read(this, "wiggle");
+ sp_event_context_read(this, "angle");
+ sp_event_context_read(this, "width");
+ sp_event_context_read(this, "thinning");
+ sp_event_context_read(this, "tremor");
+ sp_event_context_read(this, "flatness");
+ sp_event_context_read(this, "tracebackground");
+ sp_event_context_read(this, "usepressure");
+ sp_event_context_read(this, "usetilt");
+ sp_event_context_read(this, "abs_width");
+ sp_event_context_read(this, "keep_selected");
+ sp_event_context_read(this, "cap_rounding");
+
+ this->is_drawing = false;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/calligraphic/selcue")) {
+ this->enableSelectionCue();
+ }
+}
+
+void CalligraphicTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "tracebackground") {
+ this->trace_bg = val.getBool();
+ } else if (path == "keep_selected") {
+ this->keep_selected = val.getBool();
+ } else {
+ //pass on up to parent class to handle common attributes.
+ DynamicBase::set(val);
+ }
+
+ //g_print("DDC: %g %g %g %g\n", ddc->mass, ddc->drag, ddc->angle, ddc->width);
+}
+
+static double
+flerp(double f0, double f1, double p)
+{
+ return f0 + ( f1 - f0 ) * p;
+}
+
+///* Get normalized point */
+//Geom::Point CalligraphicTool::getNormalizedPoint(Geom::Point v) const {
+// Geom::Rect drect = desktop->get_display_area();
+//
+// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] );
+//
+// return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max);
+//}
+//
+///* Get view point */
+//Geom::Point CalligraphicTool::getViewPoint(Geom::Point n) const {
+// Geom::Rect drect = desktop->get_display_area();
+//
+// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] );
+//
+// return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]);
+//}
+
+void CalligraphicTool::reset(Geom::Point p) {
+ this->last = this->cur = this->getNormalizedPoint(p);
+
+ this->vel = Geom::Point(0,0);
+ this->vel_max = 0;
+ this->acc = Geom::Point(0,0);
+ this->ang = Geom::Point(0,0);
+ this->del = Geom::Point(0,0);
+}
+
+void CalligraphicTool::extinput(GdkEvent *event) {
+ if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) {
+ this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE);
+ } else {
+ this->pressure = DDC_DEFAULT_PRESSURE;
+ }
+
+ if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt)) {
+ this->xtilt = CLAMP (this->xtilt, DDC_MIN_TILT, DDC_MAX_TILT);
+ } else {
+ this->xtilt = DDC_DEFAULT_TILT;
+ }
+
+ if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt)) {
+ this->ytilt = CLAMP (this->ytilt, DDC_MIN_TILT, DDC_MAX_TILT);
+ } else {
+ this->ytilt = DDC_DEFAULT_TILT;
+ }
+}
+
+
+bool CalligraphicTool::apply(Geom::Point p) {
+ Geom::Point n = this->getNormalizedPoint(p);
+
+ /* Calculate mass and drag */
+ double const mass = flerp(1.0, 160.0, this->mass);
+ double const drag = flerp(0.0, 0.5, this->drag * this->drag);
+
+ /* Calculate force and acceleration */
+ Geom::Point force = n - this->cur;
+
+ // If force is below the absolute threshold DYNA_EPSILON,
+ // or we haven't yet reached DYNA_VEL_START (i.e. at the beginning of stroke)
+ // _and_ the force is below the (higher) DYNA_EPSILON_START threshold,
+ // discard this move.
+ // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen,
+ // especially bothersome at the start of the stroke where we don't yet have the inertia to
+ // smooth them out.
+ if ( Geom::L2(force) < DYNA_EPSILON || (this->vel_max < DYNA_VEL_START && Geom::L2(force) < DYNA_EPSILON_START)) {
+ return FALSE;
+ }
+
+ this->acc = force / mass;
+
+ /* Calculate new velocity */
+ this->vel += this->acc;
+
+ if (Geom::L2(this->vel) > this->vel_max)
+ this->vel_max = Geom::L2(this->vel);
+
+ /* Calculate angle of drawing tool */
+
+ double a1;
+ if (this->usetilt) {
+ // 1a. calculate nib angle from input device tilt:
+ if (this->xtilt == 0 && this->ytilt == 0) {
+ // to be sure that atan2 in the computation below
+ // would not crash or return NaN.
+ a1 = 0;
+ } else {
+ Geom::Point dir(-this->xtilt, this->ytilt);
+ a1 = atan2(dir);
+ }
+ }
+ else {
+ // 1b. fixed dc->angle (absolutely flat nib):
+ a1 = ( this->angle / 180.0 ) * M_PI;
+ }
+ a1 *= -this->desktop->yaxisdir();
+ a1 = fmod(a1, M_PI);
+ if (a1 > 0.5*M_PI) {
+ a1 -= M_PI;
+ } else if (a1 <= -0.5*M_PI) {
+ a1 += M_PI;
+ }
+
+ // 2. perpendicular to dc->vel (absolutely non-flat nib):
+ gdouble const mag_vel = Geom::L2(this->vel);
+ if ( mag_vel < DYNA_EPSILON ) {
+ return FALSE;
+ }
+ Geom::Point ang2 = Geom::rot90(this->vel) / mag_vel;
+
+ // 3. Average them using flatness parameter:
+ // calculate angles
+ double a2 = atan2(ang2);
+ // flip a2 to force it to be in the same half-circle as a1
+ bool flipped = false;
+ if (fabs (a2-a1) > 0.5*M_PI) {
+ a2 += M_PI;
+ flipped = true;
+ }
+ // normalize a2
+ if (a2 > M_PI)
+ a2 -= 2*M_PI;
+ if (a2 < -M_PI)
+ a2 += 2*M_PI;
+ // find the flatness-weighted bisector angle, unflip if a2 was flipped
+ // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this?
+ double new_ang = a1 + (1 - this->flatness) * (a2 - a1) - (flipped? M_PI : 0);
+
+ // Try to detect a sudden flip when the new angle differs too much from the previous for the
+ // current velocity; in that case discard this move
+ double angle_delta = Geom::L2(Geom::Point (cos (new_ang), sin (new_ang)) - this->ang);
+ if ( angle_delta / Geom::L2(this->vel) > 4000 ) {
+ return FALSE;
+ }
+
+ // convert to point
+ this->ang = Geom::Point (cos (new_ang), sin (new_ang));
+
+// g_print ("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(dc->acc), dc->vel_max, Geom::L2(dc->vel), a1, a2, new_ang);
+
+ /* Apply drag */
+ this->vel *= 1.0 - drag;
+
+ /* Update position */
+ this->last = this->cur;
+ this->cur += this->vel;
+
+ return TRUE;
+}
+
+void CalligraphicTool::brush() {
+ g_assert( this->npoints >= 0 && this->npoints < SAMPLING_SIZE );
+
+ // How much velocity thins strokestyle
+ double vel_thin = flerp (0, 160, this->vel_thin);
+
+ // Influence of pressure on thickness
+ double pressure_thick = (this->usepressure ? this->pressure : 1.0);
+
+ // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass
+ // drag)
+ Geom::Point brush = this->getViewPoint(this->cur);
+ Geom::Point brush_w = SP_EVENT_CONTEXT(this)->desktop->d2w(brush);
+
+ double trace_thick = 1;
+ if (this->trace_bg) {
+ // pick single pixel
+ double R, G, B, A;
+ Geom::IntRect area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1));
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
+ sp_canvas_arena_render_surface(SP_CANVAS_ARENA(this->desktop->getDrawing()), s, area);
+ ink_cairo_surface_average_color_premul(s, R, G, B, A);
+ cairo_surface_destroy(s);
+ double max = MAX (MAX (R, G), B);
+ double min = MIN (MIN (R, G), B);
+ double L = A * (max + min)/2 + (1 - A); // blend with white bg
+ trace_thick = 1 - L;
+ //g_print ("L %g thick %g\n", L, trace_thick);
+ }
+
+ double width = (pressure_thick * trace_thick - vel_thin * Geom::L2(this->vel)) * this->width;
+
+ double tremble_left = 0, tremble_right = 0;
+ if (this->tremor > 0) {
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double x1, x2, w, y1, y2;
+ do {
+ x1 = 2.0 * g_random_double_range(0,1) - 1.0;
+ x2 = 2.0 * g_random_double_range(0,1) - 1.0;
+ w = x1 * x1 + x2 * x2;
+ } while ( w >= 1.0 );
+ w = sqrt( (-2.0 * log( w ) ) / w );
+ y1 = x1 * w;
+ y2 = x2 * w;
+
+ // deflect both left and right edges randomly and independently, so that:
+ // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve;
+ // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths;
+ // (3) deflection somewhat depends on speed, to prevent fast strokes looking
+ // comparatively smooth and slow ones excessively jittery
+ tremble_left = (y1)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel));
+ tremble_right = (y2)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel));
+ }
+
+ if ( width < 0.02 * this->width ) {
+ width = 0.02 * this->width;
+ }
+
+ double dezoomify_factor = 0.05 * 1000;
+ if (!this->abs_width) {
+ dezoomify_factor /= SP_EVENT_CONTEXT(this)->desktop->current_zoom();
+ }
+
+ Geom::Point del_left = dezoomify_factor * (width + tremble_left) * this->ang;
+ Geom::Point del_right = dezoomify_factor * (width + tremble_right) * this->ang;
+
+ this->point1[this->npoints] = brush + del_left;
+ this->point2[this->npoints] = brush - del_right;
+
+ this->del = 0.5*(del_left + del_right);
+
+ this->npoints++;
+}
+
+static void
+sp_ddc_update_toolbox (SPDesktop *desktop, const gchar *id, double value)
+{
+ desktop->setToolboxAdjustmentValue (id, value);
+}
+
+void CalligraphicTool::cancel() {
+ this->dragging = false;
+ this->is_drawing = false;
+
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ /* Remove all temporary line segments */
+ for (auto i:this->segments)
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(i));
+ this->segments.clear();
+
+ /* reset accumulated curve */
+ this->accumulated->reset();
+ this->clear_current();
+
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+}
+
+bool CalligraphicTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+
+ this->accumulated->reset();
+
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+
+ /* initialize first point */
+ this->npoints = 0;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ ( GDK_KEY_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK ),
+ nullptr,
+ event->button.time);
+
+ ret = TRUE;
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(3);
+ set_high_motion_precision();
+ this->is_drawing = true;
+ this->just_started_drawing = true;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ {
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ this->extinput(event);
+
+ this->message_context->clear();
+
+ // for hatching:
+ double hatch_dist = 0;
+ Geom::Point hatch_unit_vector(0,0);
+ Geom::Point nearest(0,0);
+ Geom::Point pointer(0,0);
+ Geom::Affine motion_to_curve(Geom::identity());
+
+ if (event->motion.state & GDK_CONTROL_MASK) { // hatching - sense the item
+
+ SPItem *selected = desktop->getSelection()->singleItem();
+ if (selected && (SP_IS_SHAPE(selected) || SP_IS_TEXT(selected))) {
+ // One item selected, and it's a path;
+ // let's try to track it as a guide
+
+ if (selected != this->hatch_item) {
+ this->hatch_item = selected;
+ if (this->hatch_livarot_path)
+ delete this->hatch_livarot_path;
+ this->hatch_livarot_path = Path_for_item (this->hatch_item, true, true);
+ this->hatch_livarot_path->ConvertWithBackData(0.01);
+ }
+
+ // calculate pointer point in the guide item's coords
+ motion_to_curve = selected->dt2i_affine() * selected->i2doc_affine();
+ pointer = motion_dt * motion_to_curve;
+
+ // calculate the nearest point on the guide path
+ boost::optional<Path::cut_position> position = get_nearest_position_on_Path(this->hatch_livarot_path, pointer);
+ nearest = get_point_on_Path(this->hatch_livarot_path, position->piece, position->t);
+
+
+ // distance from pointer to nearest
+ hatch_dist = Geom::L2(pointer - nearest);
+ // unit-length vector
+ hatch_unit_vector = (pointer - nearest)/hatch_dist;
+
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Guide path selected</b>; start drawing along the guide with <b>Ctrl</b>"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Select a guide path</b> to track with <b>Ctrl</b>"));
+ }
+ }
+
+ if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ this->dragging = TRUE;
+
+ if (event->motion.state & GDK_CONTROL_MASK && this->hatch_item) { // hatching
+
+#define HATCH_VECTOR_ELEMENTS 12
+#define INERTIA_ELEMENTS 24
+#define SPEED_ELEMENTS 12
+#define SPEED_MIN 0.3
+#define SPEED_NORMAL 0.35
+#define INERTIA_FORCE 0.5
+
+ // speed is the movement of the nearest point along the guide path, divided by
+ // the movement of the pointer at the same period; it is averaged for the last
+ // SPEED_ELEMENTS motion events. Normally, as you track the guide path, speed
+ // is about 1, i.e. the nearest point on the path is moved by about the same
+ // distance as the pointer. If the speed starts to decrease, we are losing
+ // contact with the guide; if it drops below SPEED_MIN, we are on our own and
+ // not attracted to guide anymore. Most often this happens when you have
+ // tracked to the end of a guide calligraphic stroke and keep moving
+ // further. We try to handle this situation gracefully: not stick with the
+ // guide forever but let go of it smoothly and without sharp jerks (non-zero
+ // mass recommended; with zero mass, jerks are still quite noticeable).
+
+ double speed = 1;
+ if (Geom::L2(this->hatch_last_nearest) != 0) {
+ // the distance nearest moved since the last motion event
+ double nearest_moved = Geom::L2(nearest - this->hatch_last_nearest);
+ // the distance pointer moved since the last motion event
+ double pointer_moved = Geom::L2(pointer - this->hatch_last_pointer);
+ // store them in stacks limited to SPEED_ELEMENTS
+ this->hatch_nearest_past.push_front(nearest_moved);
+ if (this->hatch_nearest_past.size() > SPEED_ELEMENTS)
+ this->hatch_nearest_past.pop_back();
+ this->hatch_pointer_past.push_front(pointer_moved);
+ if (this->hatch_pointer_past.size() > SPEED_ELEMENTS)
+ this->hatch_pointer_past.pop_back();
+
+ // If the stacks are full,
+ if (this->hatch_nearest_past.size() == SPEED_ELEMENTS) {
+ // calculate the sums of all stored movements
+ double nearest_sum = std::accumulate (this->hatch_nearest_past.begin(), this->hatch_nearest_past.end(), 0.0);
+ double pointer_sum = std::accumulate (this->hatch_pointer_past.begin(), this->hatch_pointer_past.end(), 0.0);
+ // and divide to get the speed
+ speed = nearest_sum/pointer_sum;
+ //g_print ("nearest sum %g pointer_sum %g speed %g\n", nearest_sum, pointer_sum, speed);
+ }
+ }
+
+ if ( this->hatch_escaped // already escaped, do not reattach
+ || (speed < SPEED_MIN) // stuck; most likely reached end of traced stroke
+ || (this->hatch_spacing > 0 && hatch_dist > 50 * this->hatch_spacing) // went too far from the guide
+ ) {
+ // We are NOT attracted to the guide!
+
+ //g_print ("\nlast_nearest %g %g nearest %g %g pointer %g %g pos %d %g\n", dc->last_nearest[Geom::X], dc->last_nearest[Geom::Y], nearest[Geom::X], nearest[Geom::Y], pointer[Geom::X], pointer[Geom::Y], position->piece, position->t);
+
+ // Remember hatch_escaped so we don't get
+ // attracted again until the end of this stroke
+ this->hatch_escaped = true;
+
+ if (this->inertia_vectors.size() >= INERTIA_ELEMENTS/2) { // move by inertia
+ Geom::Point moved_past_escape = motion_dt - this->inertia_vectors.front();
+ Geom::Point inertia =
+ this->inertia_vectors.front() - this->inertia_vectors.back();
+
+ double dot = Geom::dot (moved_past_escape, inertia);
+ dot /= Geom::L2(moved_past_escape) * Geom::L2(inertia);
+
+ if (dot > 0) { // mouse is still moving in approx the same direction
+ Geom::Point should_have_moved =
+ (inertia) * (1/Geom::L2(inertia)) * Geom::L2(moved_past_escape);
+ motion_dt = this->inertia_vectors.front() +
+ (INERTIA_FORCE * should_have_moved + (1 - INERTIA_FORCE) * moved_past_escape);
+ }
+ }
+
+ } else {
+
+ // Calculate angle cosine of this vector-to-guide and all past vectors
+ // summed, to detect if we accidentally flipped to the other side of the
+ // guide
+ Geom::Point hatch_vector_accumulated = std::accumulate
+ (this->hatch_vectors.begin(), this->hatch_vectors.end(), Geom::Point(0,0));
+ double dot = Geom::dot (pointer - nearest, hatch_vector_accumulated);
+ dot /= Geom::L2(pointer - nearest) * Geom::L2(hatch_vector_accumulated);
+
+ if (this->hatch_spacing != 0) { // spacing was already set
+ double target;
+ if (speed > SPEED_NORMAL) {
+ // all ok, strictly obey the spacing
+ target = this->hatch_spacing;
+ } else {
+ // looks like we're starting to lose speed,
+ // so _gradually_ let go attraction to prevent jerks
+ target = (this->hatch_spacing * speed + hatch_dist * (SPEED_NORMAL - speed))/SPEED_NORMAL;
+ }
+ if (!std::isnan(dot) && dot < -0.5) {// flip
+ target = -target;
+ }
+
+ // This is the track pointer that we will use instead of the real one
+ Geom::Point new_pointer = nearest + target * hatch_unit_vector;
+
+ // some limited feedback: allow persistent pulling to slightly change
+ // the spacing
+ this->hatch_spacing += (hatch_dist - this->hatch_spacing)/3500;
+
+ // return it to the desktop coords
+ motion_dt = new_pointer * motion_to_curve.inverse();
+
+ if (speed >= SPEED_NORMAL) {
+ this->inertia_vectors.push_front(motion_dt);
+ if (this->inertia_vectors.size() > INERTIA_ELEMENTS)
+ this->inertia_vectors.pop_back();
+ }
+
+ } else {
+ // this is the first motion event, set the dist
+ this->hatch_spacing = hatch_dist;
+ }
+
+ // remember last points
+ this->hatch_last_pointer = pointer;
+ this->hatch_last_nearest = nearest;
+
+ this->hatch_vectors.push_front(pointer - nearest);
+ if (this->hatch_vectors.size() > HATCH_VECTOR_ELEMENTS)
+ this->hatch_vectors.pop_back();
+ }
+
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, this->hatch_escaped? _("Tracking: <b>connection to guide path lost!</b>") : _("<b>Tracking</b> a guide path"));
+
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> a calligraphic stroke"));
+ }
+
+ if (this->just_started_drawing) {
+ this->just_started_drawing = false;
+ this->reset(motion_dt);
+ }
+
+ if (!this->apply(motion_dt)) {
+ ret = TRUE;
+ break;
+ }
+
+ if ( this->cur != this->last ) {
+ this->brush();
+ g_assert( this->npoints > 0 );
+ this->fit_and_split(false);
+ }
+ ret = TRUE;
+ }
+
+ // Draw the hatching circle if necessary
+ if (event->motion.state & GDK_CONTROL_MASK) {
+ if (this->hatch_spacing == 0 && hatch_dist != 0) {
+ // Haven't set spacing yet: gray, center free, update radius live
+ Geom::Point c = desktop->w2d(motion_w);
+ Geom::Affine const sm (Geom::Scale(hatch_dist, hatch_dist) * Geom::Translate(c));
+ sp_canvas_item_affine_absolute(this->hatch_area, sm);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_show(this->hatch_area);
+ } else if (this->dragging && !this->hatch_escaped) {
+ // Tracking: green, center snapped, fixed radius
+ Geom::Point c = motion_dt;
+ Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c));
+ sp_canvas_item_affine_absolute(this->hatch_area, sm);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x00FF00ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_show(this->hatch_area);
+ } else if (this->dragging && this->hatch_escaped) {
+ // Tracking escaped: red, center free, fixed radius
+ Geom::Point c = motion_dt;
+ Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c));
+
+ sp_canvas_item_affine_absolute(this->hatch_area, sm);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0xFF0000ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_show(this->hatch_area);
+ } else {
+ // Not drawing but spacing set: gray, center snapped, fixed radius
+ Geom::Point c = (nearest + this->hatch_spacing * hatch_unit_vector) * motion_to_curve.inverse();
+ if (!std::isnan(c[Geom::X]) && !std::isnan(c[Geom::Y])) {
+ Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c));
+ sp_canvas_item_affine_absolute(this->hatch_area, sm);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_show(this->hatch_area);
+ }
+ }
+ } else {
+ sp_canvas_item_hide(this->hatch_area);
+ }
+ }
+ break;
+
+
+ case GDK_BUTTON_RELEASE:
+ {
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ desktop->canvas->endForcedFullRedraws();
+ set_high_motion_precision(false);
+ this->is_drawing = false;
+
+ if (this->dragging && event->button.button == 1 && !this->space_panning) {
+ this->dragging = FALSE;
+
+ this->apply(motion_dt);
+
+ /* Remove all temporary line segments */
+ for (auto i:this->segments)
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(i));
+ this->segments.clear();
+
+ /* Create object */
+ this->fit_and_split(true);
+ if (this->accumulate())
+ this->set_to_accumulated(event->button.state & GDK_SHIFT_MASK, event->button.state & GDK_MOD1_MASK); // performs document_done
+ else
+ g_warning ("Failed to create path: invalid data in dc->cal1 or dc->cal2");
+
+ /* reset accumulated curve */
+ this->accumulated->reset();
+
+ this->clear_current();
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+
+ if (!this->hatch_pointer_past.empty()) this->hatch_pointer_past.clear();
+ if (!this->hatch_nearest_past.empty()) this->hatch_nearest_past.clear();
+ if (!this->inertia_vectors.empty()) this->inertia_vectors.clear();
+ if (!this->hatch_vectors.empty()) this->hatch_vectors.clear();
+ this->hatch_last_nearest = Geom::Point(0,0);
+ this->hatch_last_pointer = Geom::Point(0,0);
+ this->hatch_escaped = false;
+ this->hatch_item = nullptr;
+ this->hatch_livarot_path = nullptr;
+ this->just_started_drawing = false;
+
+ if (this->hatch_spacing != 0 && !this->keep_selected) {
+ // we do not select the newly drawn path, so increase spacing by step
+ if (this->hatch_spacing_step == 0) {
+ this->hatch_spacing_step = this->hatch_spacing;
+ }
+ this->hatch_spacing += this->hatch_spacing_step;
+ }
+
+ this->message_context->clear();
+ ret = TRUE;
+ } else if (!this->dragging && event->button.button == 1 && !this->space_panning){
+ spdc_create_single_dot(this, this->desktop->w2d(motion_w), "/tools/calligraphic", event->button.state);
+ }
+ break;
+ }
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->angle += 5.0;
+ if (this->angle > 90.0)
+ this->angle = 90.0;
+ sp_ddc_update_toolbox (desktop, "calligraphy-angle", this->angle);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->angle -= 5.0;
+ if (this->angle < -90.0)
+ this->angle = -90.0;
+ sp_ddc_update_toolbox (desktop, "calligraphy-angle", this->angle);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width += 0.01;
+ if (this->width > 1.0)
+ this->width = 1.0;
+ sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100); // the same spinbutton is for alt+x
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width -= 0.01;
+ if (this->width < 0.01)
+ this->width = 0.01;
+ sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ this->width = 0.01;
+ sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100);
+ ret = TRUE;
+ break;
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ this->width = 1.0;
+ sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100);
+ ret = TRUE;
+ break;
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("calligraphy-width");
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Escape:
+ if (this->is_drawing) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (MOD__CTRL_ONLY(event) && this->is_drawing) {
+ // if drawing, cancel, otherwise pass it up for undo
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ this->message_context->clear();
+ this->hatch_spacing = 0;
+ this->hatch_spacing_step = 0;
+ break;
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+// if ((SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler) {
+// ret = (SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler(event_context, event);
+// }
+ ret = DynamicBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+
+void CalligraphicTool::clear_current() {
+ /* reset bpath */
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), nullptr);
+ /* reset curve */
+ this->currentcurve->reset();
+ this->cal1->reset();
+ this->cal2->reset();
+ /* reset points */
+ this->npoints = 0;
+}
+
+void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) {
+ if (!this->accumulated->is_empty()) {
+ if (!this->repr) {
+ /* Create object */
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+
+ /* Set style */
+ sp_desktop_apply_style_tool (desktop, repr, "/tools/calligraphic", false);
+
+ this->repr = repr;
+
+ SPItem *item=SP_ITEM(desktop->currentLayer()->appendChildRepr(this->repr));
+ Inkscape::GC::release(this->repr);
+ item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ item->updateRepr();
+ }
+
+ Geom::PathVector pathv = this->accumulated->get_pathvector() * desktop->dt2doc();
+ gchar *str = sp_svg_write_path(pathv);
+ g_assert( str != nullptr );
+ this->repr->setAttribute("d", str);
+ g_free(str);
+
+ if (unionize) {
+ desktop->getSelection()->add(this->repr);
+ desktop->getSelection()->pathUnion(true);
+ } else if (subtract) {
+ desktop->getSelection()->add(this->repr);
+ desktop->getSelection()->pathDiff(true);
+ } else {
+ if (this->keep_selected) {
+ desktop->getSelection()->set(this->repr);
+ }
+ }
+
+ // Now we need to write the transform information.
+ // First, find out whether our repr is still linked to a valid object. In this case,
+ // we need to write the transform data only for this element.
+ // Either there was no boolean op or it failed.
+ SPItem *result = SP_ITEM(desktop->doc()->getObjectByRepr(this->repr));
+
+ if (result == nullptr) {
+ // The boolean operation succeeded.
+ // Now we fetch the single item, that has been set as selected by the boolean op.
+ // This is its result.
+ result = desktop->getSelection()->singleItem();
+ }
+ result->doWriteTransform(result->transform, nullptr, true);
+ } else {
+ if (this->repr) {
+ sp_repr_unparent(this->repr);
+ }
+
+ this->repr = nullptr;
+ }
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_CALLIGRAPHIC,
+ _("Draw calligraphic stroke"));
+}
+
+static void
+add_cap(SPCurve *curve,
+ Geom::Point const &from,
+ Geom::Point const &to,
+ double rounding)
+{
+ if (Geom::L2( to - from ) > DYNA_EPSILON) {
+ Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0);
+ double mag = Geom::L2(vel);
+
+ Geom::Point v = mag * Geom::rot90( to - from ) / Geom::L2( to - from );
+ curve->curveto(from + v, to + v, to);
+ }
+}
+
+bool CalligraphicTool::accumulate() {
+ if (
+ this->cal1->is_empty() ||
+ this->cal2->is_empty() ||
+ (this->cal1->get_segment_count() <= 0) ||
+ this->cal1->first_path()->closed()
+ ) {
+
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return false; // failure
+ }
+
+ SPCurve *rev_cal2 = this->cal2->create_reverse();
+
+ if ((rev_cal2->get_segment_count() <= 0) || rev_cal2->first_path()->closed()) {
+ rev_cal2->unref();
+
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return false; // failure
+ }
+
+ Geom::Curve const * dc_cal1_firstseg = this->cal1->first_segment();
+ Geom::Curve const * rev_cal2_firstseg = rev_cal2->first_segment();
+ Geom::Curve const * dc_cal1_lastseg = this->cal1->last_segment();
+ Geom::Curve const * rev_cal2_lastseg = rev_cal2->last_segment();
+
+ this->accumulated->reset(); /* Is this required ?? */
+
+ this->accumulated->append(this->cal1, false);
+
+ add_cap(this->accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), this->cap_rounding);
+
+ this->accumulated->append(rev_cal2, true);
+
+ add_cap(this->accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), this->cap_rounding);
+
+ this->accumulated->closepath();
+
+ rev_cal2->unref();
+
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return true; // success
+}
+
+static double square(double const x)
+{
+ return x * x;
+}
+
+void CalligraphicTool::fit_and_split(bool release) {
+ double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC );
+
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[F&S:R=%c]", release?'T':'F');
+#endif
+
+ if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) {
+ return; // just clicked
+ }
+
+ if ( this->npoints == SAMPLING_SIZE - 1 || release ) {
+#define BEZIER_SIZE 4
+#define BEZIER_MAX_BEZIERS 8
+#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS )
+
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[F&S:#] dc->npoints:%d, release:%s\n",
+ this->npoints, release ? "TRUE" : "FALSE");
+#endif
+
+ /* Current calligraphic */
+ if ( this->cal1->is_empty() || this->cal2->is_empty() ) {
+ /* dc->npoints > 0 */
+ /* g_print("calligraphics(1|2) reset\n"); */
+ this->cal1->reset();
+ this->cal2->reset();
+
+ this->cal1->moveto(this->point1[0]);
+ this->cal2->moveto(this->point2[0]);
+ }
+
+ Geom::Point b1[BEZIER_MAX_LENGTH];
+ gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints,
+ tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) );
+
+ Geom::Point b2[BEZIER_MAX_LENGTH];
+ gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints,
+ tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) );
+
+ if ( nb1 != -1 && nb2 != -1 ) {
+ /* Fit and draw and reset state */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("nb1:%d nb2:%d\n", nb1, nb2);
+#endif
+ /* CanvasShape */
+ if (! release) {
+ this->currentcurve->reset();
+ this->currentcurve->moveto(b1[0]);
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]);
+ for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) {
+ this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]);
+ }
+ // FIXME: dc->segments is always NULL at this point??
+ if (this->segments.empty()) { // first segment
+ add_cap(this->currentcurve, b2[0], b1[0], this->cap_rounding);
+ }
+ this->currentcurve->closepath();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true);
+ }
+
+ /* Current calligraphic */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->cal1->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
+ this->cal2->curveto(bp2[1], bp2[2], bp2[3]);
+ }
+ } else {
+ /* fixme: ??? */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[fit_and_split] failed to fit-cubic.\n");
+#endif
+ this->draw_temporary_box();
+
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal1->lineto(this->point1[i]);
+ }
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal2->lineto(this->point2[i]);
+ }
+ }
+
+ /* Fit and draw and copy last point */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[%d]Yup\n", this->npoints);
+#endif
+ if (!release) {
+ g_assert(!this->currentcurve->is_empty());
+
+ SPCanvasItem *cbp = sp_canvas_item_new(desktop->getSketch(),
+ SP_TYPE_CANVAS_BPATH,
+ nullptr);
+ SPCurve *curve = this->currentcurve->copy();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve, true);
+ curve->unref();
+
+ guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", true);
+ //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", false);
+ double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/calligraphic");
+ double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", true);
+ //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", false);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD);
+ //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing
+ //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ /* fixme: Cannot we cascade it to root more clearly? */
+ g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop);
+
+ this->segments.push_back(cbp);
+ }
+
+ this->point1[0] = this->point1[this->npoints - 1];
+ this->point2[0] = this->point2[this->npoints - 1];
+ this->npoints = 1;
+ } else {
+ this->draw_temporary_box();
+ }
+}
+
+void CalligraphicTool::draw_temporary_box() {
+ this->currentcurve->reset();
+
+ this->currentcurve->moveto(this->point2[this->npoints-1]);
+
+ for (gint i = this->npoints-2; i >= 0; i--) {
+ this->currentcurve->lineto(this->point2[i]);
+ }
+
+ for (gint i = 0; i < this->npoints; i++) {
+ this->currentcurve->lineto(this->point1[i]);
+ }
+
+ if (this->npoints >= 2) {
+ add_cap(this->currentcurve, this->point1[this->npoints-1], this->point2[this->npoints-1], this->cap_rounding);
+ }
+
+ this->currentcurve->closepath();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, 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/tools/calligraphic-tool.h b/src/ui/tools/calligraphic-tool.h
new file mode 100644
index 0000000..924d018
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.h
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SP_DYNA_DRAW_CONTEXT_H_SEEN
+#define SP_DYNA_DRAW_CONTEXT_H_SEEN
+
+/*
+ * Handwriting-like drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <list>
+#include <string>
+
+#include <2geom/point.h>
+
+#include "ui/tools/dynamic-base.h"
+
+class SPItem;
+class Path;
+struct SPCanvasItem;
+
+#define DDC_MIN_PRESSURE 0.0
+#define DDC_MAX_PRESSURE 1.0
+#define DDC_DEFAULT_PRESSURE 1.0
+
+#define DDC_MIN_TILT -1.0
+#define DDC_MAX_TILT 1.0
+#define DDC_DEFAULT_TILT 0.0
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class CalligraphicTool : public DynamicBase {
+public:
+ CalligraphicTool();
+ ~CalligraphicTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ /** newly created object remain selected */
+ bool keep_selected;
+
+ double hatch_spacing;
+ double hatch_spacing_step;
+ SPItem *hatch_item;
+ Path *hatch_livarot_path;
+ std::list<double> hatch_nearest_past;
+ std::list<double> hatch_pointer_past;
+ std::list<Geom::Point> inertia_vectors;
+ Geom::Point hatch_last_nearest, hatch_last_pointer;
+ std::list<Geom::Point> hatch_vectors;
+ bool hatch_escaped;
+ SPCanvasItem *hatch_area;
+ bool just_started_drawing;
+ bool trace_bg;
+
+ void clear_current();
+ void set_to_accumulated(bool unionize, bool subtract);
+ bool accumulate();
+ void fit_and_split(bool release);
+ void draw_temporary_box();
+ void cancel();
+ void brush();
+ bool apply(Geom::Point p);
+ void extinput(GdkEvent *event);
+ void reset(Geom::Point p);
+};
+
+}
+}
+}
+
+#endif // SP_DYNA_DRAW_CONTEXT_H_SEEN
+
+/*
+ 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/tools/connector-tool.cpp b/src/ui/tools/connector-tool.cpp
new file mode 100644
index 0000000..d3425a2
--- /dev/null
+++ b/src/ui/tools/connector-tool.cpp
@@ -0,0 +1,1393 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Connector creation tool
+ *
+ * Authors:
+ * Michael Wybrow <mjwybrow@users.sourceforge.net>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ * Martin Owens <doctormo@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Michael Wybrow
+ * Copyright (C) 2009 Monash University
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ * TODO:
+ * o Show a visual indicator for objects with the 'avoid' property set.
+ * o Allow user to change a object between a path and connector through
+ * the interface.
+ * o Create an interface for setting markers (arrow heads).
+ * o Better distinguish between paths and connectors to prevent problems
+ * in the node tool and paths accidentally being turned into connectors
+ * in the connector tool. Perhaps have a way to convert between.
+ * o Only call libavoid's updateEndPoint as required. Currently we do it
+ * for both endpoints, even if only one is moving.
+ * o Deal sanely with connectors with both endpoints attached to the
+ * same connection point, and drawing of connectors attaching
+ * overlapping shapes (currently tries to adjust connector to be
+ * outside both bounding boxes).
+ * o Fix many special cases related to connectors updating,
+ * e.g., copying a couple of shapes and a connector that are
+ * attached to each other.
+ * e.g., detach connector when it is moved or transformed in
+ * one of the other contexts.
+ * o Cope with shapes whose ids change when they have attached
+ * connectors.
+ * o During dragging motion, gobble up to and use the final motion event.
+ * Gobbling away all duplicates after the current can occasionally result
+ * in the path lagging behind the mouse cursor if it is no longer being
+ * dragged.
+ * o Fix up libavoid's representation after undo actions. It doesn't see
+ * any transform signals and hence doesn't know shapes have moved back to
+ * there earlier positions.
+ *
+ * ----------------------------------------------------------------------------
+ *
+ * Notes:
+ *
+ * Much of the way connectors work for user-defined points has been
+ * changed so that it no longer defines special attributes to record
+ * the points. Instead it uses single node paths to define points
+ * who are then separate objects that can be fixed on the canvas,
+ * grouped into objects and take full advantage of all transform, snap
+ * and align functionality of all other objects.
+ *
+ * I think that the style change between polyline and orthogonal
+ * would be much clearer with two buttons (radio behaviour -- just
+ * one is true).
+ *
+ * The other tools show a label change from "New:" to "Change:"
+ * depending on whether an object is selected. We could consider
+ * this but there may not be space.
+ *
+ * Likewise for the avoid/ignore shapes buttons. These should be
+ * inactive when a shape is not selected in the connector context.
+ *
+ */
+
+#include <string>
+#include <cstring>
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection.h"
+#include "snap.h"
+#include "verbs.h"
+
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+#include "display/sp-canvas.h"
+
+#include "3rdparty/adaptagrams/libavoid/router.h"
+
+#include "object/sp-conn-end.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+#include "object/sp-text.h"
+
+#include "ui/pixmaps/cursor-connector.xpm"
+
+#include "svg/svg.h"
+
+#include "ui/tools/connector-tool.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void cc_clear_active_knots(SPKnotList k);
+
+static void shape_event_attr_deleted(Inkscape::XML::Node *repr,
+ Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data);
+
+static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const *old_value, gchar const *new_value, bool is_interactive,
+ gpointer data);
+
+static void cc_select_handle(SPKnot* knot);
+static void cc_deselect_handle(SPKnot* knot);
+static bool cc_item_is_shape(SPItem *item);
+
+/*static Geom::Point connector_drag_origin_w(0, 0);
+static bool connector_within_tolerance = false;*/
+
+static Inkscape::XML::NodeEventVector shape_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_added */
+ shape_event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+static Inkscape::XML::NodeEventVector layer_repr_events = {
+ nullptr, /* child_added */
+ shape_event_attr_deleted,
+ nullptr, /* child_added */
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+std::string const& ConnectorTool::getPrefsPath()
+{
+ return ConnectorTool::prefsPath;
+}
+
+std::string const ConnectorTool::prefsPath = "/tools/connector";
+
+ConnectorTool::ConnectorTool()
+ : ToolBase(cursor_connector_xpm)
+ , selection(nullptr)
+ , npoints(0)
+ , state(SP_CONNECTOR_CONTEXT_IDLE)
+ , red_bpath(nullptr)
+ , red_curve(nullptr)
+ , red_color(0xff00007f)
+ , green_curve(nullptr)
+ , newconn(nullptr)
+ , newConnRef(nullptr)
+ , curvature(0.0)
+ , isOrthogonal(false)
+ , active_shape(nullptr)
+ , active_shape_repr(nullptr)
+ , active_shape_layer_repr(nullptr)
+ , active_conn(nullptr)
+ , active_conn_repr(nullptr)
+ , active_handle(nullptr)
+ , selected_handle(nullptr)
+ , clickeditem(nullptr)
+ , clickedhandle(nullptr)
+ , shref(nullptr)
+ , ehref(nullptr)
+ , c0(nullptr)
+ , c1(nullptr)
+ , cl0(nullptr)
+ , cl1(nullptr)
+{
+ for (int i = 0; i < 2; ++i) {
+ this->endpt_handle[i] = nullptr;
+ this->endpt_handler_id[i] = 0;
+ }
+}
+
+ConnectorTool::~ConnectorTool()
+{
+ this->sel_changed_connection.disconnect();
+
+ for (auto & i : this->endpt_handle) {
+ if (this->endpt_handle[1]) {
+ //g_object_unref(this->endpt_handle[i]);
+ knot_unref(i);
+ i = nullptr;
+ }
+ }
+
+ if (this->shref) {
+ g_free(this->shref);
+ this->shref = nullptr;
+ }
+
+ if (this->ehref) {
+ g_free(this->shref);
+ this->shref = nullptr;
+ }
+
+ g_assert( this->newConnRef == nullptr );
+}
+
+void ConnectorTool::setup()
+{
+ ToolBase::setup();
+
+ this->selection = this->desktop->getSelection();
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = this->selection->connectChanged(
+ sigc::mem_fun(this, &ConnectorTool::_selectionChanged)
+ );
+
+ /* Create red bpath */
+ this->red_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color,
+ 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->red_bpath), 0x00000000,
+ SP_WIND_RULE_NONZERO);
+ /* Create red curve */
+ this->red_curve = new SPCurve();
+
+ /* Create green curve */
+ this->green_curve = new SPCurve();
+
+ // Notice the initial selection.
+ //cc_selection_changed(this->selection, (gpointer) this);
+ this->_selectionChanged(this->selection);
+
+ this->within_tolerance = false;
+
+ sp_event_context_read(this, "curvature");
+ sp_event_context_read(this, "orthogonal");
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/connector/selcue", false)) {
+ this->enableSelectionCue();
+ }
+
+ // Make sure we see all enter events for canvas items,
+ // even if a mouse button is depressed.
+ this->desktop->canvas->_gen_all_enter_events = true;
+}
+
+void ConnectorTool::set(const Inkscape::Preferences::Entry& val)
+{
+ /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like
+ * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */
+ Glib::ustring name = val.getEntryName();
+
+ if (name == "curvature") {
+ this->curvature = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up
+ } else if (name == "orthogonal") {
+ this->isOrthogonal = val.getBool();
+ }
+}
+
+void ConnectorTool::finish()
+{
+ this->_finish();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+
+ ToolBase::finish();
+
+ if (this->selection) {
+ this->selection = nullptr;
+ }
+
+ this->cc_clear_active_shape();
+ this->cc_clear_active_conn();
+
+ // Restore the default event generating behaviour.
+ this->desktop->canvas->_gen_all_enter_events = false;
+}
+
+//-----------------------------------------------------------------------------
+
+
+void ConnectorTool::cc_clear_active_shape()
+{
+ if (this->active_shape == nullptr) {
+ return;
+ }
+ g_assert( this->active_shape_repr );
+ g_assert( this->active_shape_layer_repr );
+
+ this->active_shape = nullptr;
+
+ if (this->active_shape_repr) {
+ sp_repr_remove_listener_by_data(this->active_shape_repr, this);
+ Inkscape::GC::release(this->active_shape_repr);
+ this->active_shape_repr = nullptr;
+
+ sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this);
+ Inkscape::GC::release(this->active_shape_layer_repr);
+ this->active_shape_layer_repr = nullptr;
+ }
+
+ cc_clear_active_knots(this->knots);
+}
+
+static void cc_clear_active_knots(SPKnotList k)
+{
+ // Hide the connection points if they exist.
+ if (k.size()) {
+ for (auto & it : k) {
+ it.first->hide();
+ }
+ }
+}
+
+void ConnectorTool::cc_clear_active_conn()
+{
+ if (this->active_conn == nullptr) {
+ return;
+ }
+ g_assert( this->active_conn_repr );
+
+ this->active_conn = nullptr;
+
+ if (this->active_conn_repr) {
+ sp_repr_remove_listener_by_data(this->active_conn_repr, this);
+ Inkscape::GC::release(this->active_conn_repr);
+ this->active_conn_repr = nullptr;
+ }
+
+ // Hide the endpoint handles.
+ for (auto & i : this->endpt_handle) {
+ if (i) {
+ i->hide();
+ }
+ }
+}
+
+
+bool ConnectorTool::_ptHandleTest(Geom::Point& p, gchar **href)
+{
+ if (this->active_handle && (this->knots.find(this->active_handle) != this->knots.end())) {
+ p = this->active_handle->pos;
+ *href = g_strdup_printf("#%s", this->active_handle->owner->getId());
+ return true;
+ }
+ *href = nullptr;
+ return false;
+}
+
+static void cc_select_handle(SPKnot* knot)
+{
+ knot->setShape(SP_KNOT_SHAPE_SQUARE);
+ knot->setSize(10);
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff);
+ knot->updateCtrl();
+}
+
+static void cc_deselect_handle(SPKnot* knot)
+{
+ knot->setShape(SP_KNOT_SHAPE_SQUARE);
+ knot->setSize(8);
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff);
+ knot->updateCtrl();
+}
+
+bool ConnectorTool::item_handler(SPItem* item, GdkEvent* event)
+{
+ bool ret = false;
+
+ Geom::Point p(event->button.x, event->button.y);
+
+ switch (event->type) {
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && !this->space_panning) {
+ if ((this->state == SP_CONNECTOR_CONTEXT_DRAGGING) && this->within_tolerance) {
+ this->_resetColors();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ }
+
+ if (this->state != SP_CONNECTOR_CONTEXT_IDLE) {
+ // Doing something else like rerouting.
+ break;
+ }
+
+ // find out clicked item, honoring Alt
+ SPItem *item = sp_event_context_find_item(desktop, p, event->button.state & GDK_MOD1_MASK, FALSE);
+
+ if (event->button.state & GDK_SHIFT_MASK) {
+ this->selection->toggle(item);
+ } else {
+ this->selection->set(item);
+ /* When selecting a new item, do not allow showing
+ connection points on connectors. (yet?)
+ */
+
+ if (item != this->active_shape && !cc_item_is_connector(item)) {
+ this->_setActiveShape(item);
+ }
+ }
+
+ ret = true;
+ }
+ break;
+
+ case GDK_ENTER_NOTIFY:
+ if (!this->selected_handle) {
+ if (cc_item_is_shape(item)) {
+ this->_setActiveShape(item);
+ }
+
+ ret = true;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+bool ConnectorTool::root_handler(GdkEvent* event)
+{
+ bool ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = this->_handleButtonPress(event->button);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ ret = this->_handleMotionNotify(event->motion);
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ ret = this->_handleButtonRelease(event->button);
+ break;
+
+ case GDK_KEY_PRESS:
+ ret = this->_handleKeyPress(get_latin_keyval (&event->key));
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+
+bool ConnectorTool::_handleButtonPress(GdkEventButton const &bevent)
+{
+ Geom::Point const event_w(bevent.x, bevent.y);
+ /* Find desktop coordinates */
+ Geom::Point p = this->desktop->w2d(event_w);
+
+ bool ret = false;
+
+ if ( bevent.button == 1 && !this->space_panning ) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return true;
+ }
+
+ Geom::Point const event_w(bevent.x, bevent.y);
+
+ this->xp = bevent.x;
+ this->yp = bevent.y;
+ this->within_tolerance = true;
+
+ Geom::Point const event_dt = this->desktop->w2d(event_w);
+
+ SnapManager &m = this->desktop->namedview->snap_manager;
+
+ switch (this->state) {
+ case SP_CONNECTOR_CONTEXT_STOP:
+ /* This is allowed, if we just canceled curve */
+ case SP_CONNECTOR_CONTEXT_IDLE:
+ {
+ if ( this->npoints == 0 ) {
+ this->cc_clear_active_conn();
+
+ this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new connector"));
+
+ /* Set start anchor */
+ /* Create green anchor */
+ Geom::Point p = event_dt;
+
+ // Test whether we clicked on a connection point
+ bool found = this->_ptHandleTest(p, &this->shref);
+
+ if (!found) {
+ // This is the first point, so just snap it to the grid
+ // as there's no other points to go off.
+ m.setup(this->desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+ }
+ this->_setInitialPoint(p);
+
+ }
+ this->state = SP_CONNECTOR_CONTEXT_DRAGGING;
+ ret = true;
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_DRAGGING:
+ {
+ // This is the second click of a connector creation.
+ m.setup(this->desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ this->_setSubsequentPoint(p);
+ this->_finishSegment(p);
+
+ this->_ptHandleTest(p, &this->ehref);
+ if (this->npoints != 0) {
+ this->_finish();
+ }
+ this->cc_set_active_conn(this->newconn);
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ ret = true;
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_CLOSE:
+ {
+ g_warning("Button down in CLOSE state");
+ break;
+ }
+ default:
+ break;
+ }
+ } else if (bevent.button == 3) {
+ if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) {
+ // A context menu is going to be triggered here,
+ // so end the rerouting operation.
+ this->_reroutingFinish(&p);
+
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+
+ // Don't set ret to TRUE, so we drop through to the
+ // parent handler which will open the context menu.
+ } else if (this->npoints != 0) {
+ this->_finish();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ ret = true;
+ }
+ }
+ return ret;
+}
+
+bool ConnectorTool::_handleMotionNotify(GdkEventMotion const &mevent)
+{
+ bool ret = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (this->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) {
+ // allow middle-button scrolling
+ return false;
+ }
+
+ Geom::Point const event_w(mevent.x, mevent.y);
+
+ if (this->within_tolerance) {
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ if ( ( abs( (gint) mevent.x - this->xp ) < this->tolerance ) &&
+ ( abs( (gint) mevent.y - this->yp ) < this->tolerance ) ) {
+ return false; // Do not drag if we're within tolerance from origin.
+ }
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process
+ // the motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ /* Find desktop coordinates */
+ Geom::Point p = desktop->w2d(event_w);
+
+ SnapManager &m = desktop->namedview->snap_manager;
+
+ switch (this->state) {
+ case SP_CONNECTOR_CONTEXT_DRAGGING:
+ {
+ gobble_motion_events(mevent.state);
+ // This is movement during a connector creation.
+ if ( this->npoints > 0 ) {
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+ this->selection->clear();
+ this->_setSubsequentPoint(p);
+ ret = true;
+ }
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_REROUTING:
+ {
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ g_assert( SP_IS_PATH(this->clickeditem));
+
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ // Update the hidden path
+ Geom::Affine i2d ( (this->clickeditem)->i2dt_affine() );
+ Geom::Affine d2i = i2d.inverse();
+ SPPath *path = SP_PATH(this->clickeditem);
+ SPCurve *curve = path->getCurve(true);
+ if (this->clickedhandle == this->endpt_handle[0]) {
+ Geom::Point o = this->endpt_handle[1]->pos;
+ curve->stretch_endpoints(p * d2i, o * d2i);
+ } else {
+ Geom::Point o = this->endpt_handle[0]->pos;
+ curve->stretch_endpoints(o * d2i, p * d2i);
+ }
+ sp_conn_reroute_path_immediate(path);
+
+ // Copy this to the temporary visible path
+ this->red_curve = path->getCurveForEdit();
+ this->red_curve->transform(i2d);
+
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+ ret = true;
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_STOP:
+ /* This is perfectly valid */
+ break;
+ default:
+ if (!this->sp_event_context_knot_mouseover()) {
+ m.setup(desktop);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE));
+ m.unSetup();
+ }
+ break;
+ }
+ return ret;
+}
+
+bool ConnectorTool::_handleButtonRelease(GdkEventButton const &revent)
+{
+ bool ret = false;
+
+ if ( revent.button == 1 && !this->space_panning ) {
+ SPDocument *doc = desktop->getDocument();
+ SnapManager &m = desktop->namedview->snap_manager;
+
+ Geom::Point const event_w(revent.x, revent.y);
+
+ /* Find desktop coordinates */
+ Geom::Point p = this->desktop->w2d(event_w);
+
+ switch (this->state) {
+ //case SP_CONNECTOR_CONTEXT_POINT:
+ case SP_CONNECTOR_CONTEXT_DRAGGING:
+ {
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ if (this->within_tolerance) {
+ this->_finishSegment(p);
+ return true;
+ }
+ // Connector has been created via a drag, end it now.
+ this->_setSubsequentPoint(p);
+ this->_finishSegment(p);
+ // Test whether we clicked on a connection point
+ this->_ptHandleTest(p, &this->ehref);
+ if (this->npoints != 0) {
+ this->_finish();
+ }
+ this->cc_set_active_conn(this->newconn);
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_REROUTING:
+ {
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+ this->_reroutingFinish(&p);
+
+ doc->ensureUpToDate();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ return true;
+ break;
+ }
+ case SP_CONNECTOR_CONTEXT_STOP:
+ /* This is allowed, if we just cancelled curve */
+ break;
+ default:
+ break;
+ }
+ ret = true;
+ }
+ return ret;
+}
+
+bool ConnectorTool::_handleKeyPress(guint const keyval)
+{
+ bool ret = false;
+
+ switch (keyval) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (this->npoints != 0) {
+ this->_finish();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Escape:
+ if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) {
+ SPDocument *doc = desktop->getDocument();
+
+ this->_reroutingFinish(nullptr);
+
+ DocumentUndo::undo(doc);
+
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+ desktop->messageStack()->flash( Inkscape::NORMAL_MESSAGE,
+ _("Connector endpoint drag cancelled."));
+ ret = true;
+ } else if (this->npoints != 0) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->state = SP_CONNECTOR_CONTEXT_STOP;
+ this->_resetColors();
+ ret = true;
+ }
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+void ConnectorTool::_reroutingFinish(Geom::Point *const p)
+{
+ SPDocument *doc = desktop->getDocument();
+
+ // Clear the temporary path:
+ this->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+
+ if (p != nullptr) {
+ // Test whether we clicked on a connection point
+ gchar *shape_label;
+ bool found = this->_ptHandleTest(*p, &shape_label);
+
+ if (found) {
+ if (this->clickedhandle == this->endpt_handle[0]) {
+ this->clickeditem->setAttribute("inkscape:connection-start", shape_label);
+ } else {
+ this->clickeditem->setAttribute("inkscape:connection-end", shape_label);
+ }
+ g_free(shape_label);
+ }
+ }
+ this->clickeditem->setHidden(false);
+ sp_conn_reroute_path_immediate(SP_PATH(this->clickeditem));
+ this->clickeditem->updateRepr();
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, _("Reroute connector"));
+ this->cc_set_active_conn(this->clickeditem);
+}
+
+
+void ConnectorTool::_resetColors()
+{
+ /* Red */
+ this->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+
+ this->green_curve->reset();
+ this->npoints = 0;
+}
+
+void ConnectorTool::_setInitialPoint(Geom::Point const p)
+{
+ g_assert( this->npoints == 0 );
+
+ this->p[0] = p;
+ this->p[1] = p;
+ this->npoints = 2;
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+}
+
+void ConnectorTool::_setSubsequentPoint(Geom::Point const p)
+{
+ g_assert( this->npoints != 0 );
+
+ Geom::Point o = desktop->dt2doc(this->p[0]);
+ Geom::Point d = desktop->dt2doc(p);
+ Avoid::Point src(o[Geom::X], o[Geom::Y]);
+ Avoid::Point dst(d[Geom::X], d[Geom::Y]);
+
+ if (!this->newConnRef) {
+ Avoid::Router *router = desktop->getDocument()->getRouter();
+ this->newConnRef = new Avoid::ConnRef(router);
+ this->newConnRef->setEndpoint(Avoid::VertID::src, src);
+ if (this->isOrthogonal)
+ this->newConnRef->setRoutingType(Avoid::ConnType_Orthogonal);
+ else
+ this->newConnRef->setRoutingType(Avoid::ConnType_PolyLine);
+ }
+ // Set new endpoint.
+ this->newConnRef->setEndpoint(Avoid::VertID::tar, dst);
+ // Immediately generate new routes for connector.
+ this->newConnRef->makePathInvalid();
+ this->newConnRef->router()->processTransaction();
+ // Recreate curve from libavoid route.
+ recreateCurve( this->red_curve, this->newConnRef, this->curvature );
+ this->red_curve->transform(desktop->doc2dt());
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+}
+
+
+/**
+ * Concats red, blue and green.
+ * If any anchors are defined, process these, optionally removing curves from white list
+ * Invoke _flush_white to write result back to object.
+ */
+void ConnectorTool::_concatColorsAndFlush()
+{
+ SPCurve *c = this->green_curve;
+ this->green_curve = new SPCurve();
+
+ this->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+
+ if (c->is_empty()) {
+ c->unref();
+ return;
+ }
+
+ this->_flushWhite(c);
+
+ c->unref();
+}
+
+
+/*
+ * Flushes white curve(s) and additional curve into object
+ *
+ * No cleaning of colored curves - this has to be done by caller
+ * No rereading of white data, so if you cannot rely on ::modified, do it in caller
+ *
+ */
+
+void ConnectorTool::_flushWhite(SPCurve *gc)
+{
+ SPCurve *c;
+
+ if (gc) {
+ c = gc;
+ c->ref();
+ } else {
+ return;
+ }
+
+ /* Now we have to go back to item coordinates at last */
+ c->transform(this->desktop->dt2doc());
+
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ if ( c && !c->is_empty() ) {
+ /* We actually have something to write */
+
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ /* Set style */
+ sp_desktop_apply_style_tool(desktop, repr, "/tools/connector", false);
+
+ gchar *str = sp_svg_write_path( c->get_pathvector() );
+ g_assert( str != nullptr );
+ repr->setAttribute("d", str);
+ g_free(str);
+
+ /* Attach repr */
+ this->newconn = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));
+ this->newconn->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+
+ bool connection = false;
+ this->newconn->setAttribute( "inkscape:connector-type",
+ this->isOrthogonal ? "orthogonal" : "polyline", nullptr );
+ this->newconn->setAttribute( "inkscape:connector-curvature",
+ Glib::Ascii::dtostr(this->curvature).c_str(), nullptr );
+ if (this->shref) {
+ this->newconn->setAttribute( "inkscape:connection-start", this->shref);
+ connection = true;
+ }
+
+ if (this->ehref) {
+ this->newconn->setAttribute( "inkscape:connection-end", this->ehref);
+ connection = true;
+ }
+ // Process pending updates.
+ this->newconn->updateRepr();
+ doc->ensureUpToDate();
+
+ if (connection) {
+ // Adjust endpoints to shape edge.
+ sp_conn_reroute_path_immediate(SP_PATH(this->newconn));
+ this->newconn->updateRepr();
+ }
+
+ this->newconn->doWriteTransform(this->newconn->transform, nullptr, true);
+
+ // Only set the selection after we are finished with creating the attributes of
+ // the connector. Otherwise, the selection change may alter the defaults for
+ // values like curvature in the connector context, preventing subsequent lookup
+ // of their original values.
+ this->selection->set(repr);
+ Inkscape::GC::release(repr);
+ }
+
+ c->unref();
+
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, _("Create connector"));
+}
+
+
+void ConnectorTool::_finishSegment(Geom::Point const /*p*/)
+{
+ if (!this->red_curve->is_empty()) {
+ this->green_curve->append_continuous(this->red_curve, 0.0625);
+
+ this->p[0] = this->p[3];
+ this->p[1] = this->p[4];
+ this->npoints = 2;
+
+ this->red_curve->reset();
+ }
+}
+
+void ConnectorTool::_finish()
+{
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing connector"));
+
+ this->red_curve->reset();
+ this->_concatColorsAndFlush();
+
+ this->npoints = 0;
+
+ if (this->newConnRef) {
+ this->newConnRef->router()->deleteConnector(this->newConnRef);
+ this->newConnRef = nullptr;
+ }
+}
+
+
+static gboolean cc_generic_knot_handler(SPCanvasItem *, GdkEvent *event, SPKnot *knot)
+{
+ g_assert (knot != nullptr);
+
+ //g_object_ref(knot);
+ knot_ref(knot);
+
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(
+ knot->desktop->event_context);
+
+ gboolean consumed = FALSE;
+
+ gchar const *knot_tip = "Click to join at this point";
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ knot->setFlag(SP_KNOT_MOUSEOVER, TRUE);
+
+ cc->active_handle = knot;
+ if (knot_tip) {
+ knot->desktop->event_context->defaultMessageContext()->set(
+ Inkscape::NORMAL_MESSAGE, knot_tip);
+ }
+
+ consumed = TRUE;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ knot->setFlag(SP_KNOT_MOUSEOVER, FALSE);
+
+ /* FIXME: the following test is a workaround for LP Bug #1273510.
+ * It seems that a signal is not correctly disconnected, maybe
+ * something missing in cc_clear_active_conn()? */
+ if (cc) {
+ cc->active_handle = nullptr;
+ }
+
+ if (knot_tip) {
+ knot->desktop->event_context->defaultMessageContext()->clear();
+ }
+
+ consumed = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ //g_object_unref(knot);
+ knot_unref(knot);
+
+ return consumed;
+}
+
+
+static gboolean endpt_handler(SPKnot */*knot*/, GdkEvent *event, ConnectorTool *cc)
+{
+ //g_assert( SP_IS_CONNECTOR_CONTEXT(cc) );
+
+ gboolean consumed = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ g_assert( (cc->active_handle == cc->endpt_handle[0]) ||
+ (cc->active_handle == cc->endpt_handle[1]) );
+ if (cc->state == SP_CONNECTOR_CONTEXT_IDLE) {
+ cc->clickeditem = cc->active_conn;
+ cc->clickedhandle = cc->active_handle;
+ cc->cc_clear_active_conn();
+ cc->state = SP_CONNECTOR_CONTEXT_REROUTING;
+
+ // Disconnect from attached shape
+ unsigned ind = (cc->active_handle == cc->endpt_handle[0]) ? 0 : 1;
+ sp_conn_end_detach(cc->clickeditem, ind);
+
+ Geom::Point origin;
+ if (cc->clickedhandle == cc->endpt_handle[0]) {
+ origin = cc->endpt_handle[1]->pos;
+ } else {
+ origin = cc->endpt_handle[0]->pos;
+ }
+
+ // Show the red path for dragging.
+ cc->red_curve = SP_PATH(cc->clickeditem)->getCurveForEdit();
+ Geom::Affine i2d = (cc->clickeditem)->i2dt_affine();
+ cc->red_curve->transform(i2d);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), cc->red_curve, true);
+
+ cc->clickeditem->setHidden(true);
+
+ // The rest of the interaction rerouting the connector is
+ // handled by the context root handler.
+ consumed = TRUE;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return consumed;
+}
+
+void ConnectorTool::_activeShapeAddKnot(SPItem* item)
+{
+ SPKnot *knot = new SPKnot(desktop, nullptr);
+
+ knot->owner = item;
+ knot->setShape(SP_KNOT_SHAPE_SQUARE);
+ knot->setSize(8);
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff);
+ knot->updateCtrl();
+
+ // We don't want to use the standard knot handler.
+ g_signal_handler_disconnect(G_OBJECT(knot->item),
+ knot->_event_handler_id);
+
+ knot->_event_handler_id = 0;
+
+ g_signal_connect(G_OBJECT(knot->item), "event",
+ G_CALLBACK(cc_generic_knot_handler), knot);
+
+ knot->setPosition(item->getAvoidRef().getConnectionPointPos() * desktop->doc2dt(), 0);
+ knot->show();
+ this->knots[knot] = 1;
+}
+
+void ConnectorTool::_setActiveShape(SPItem *item)
+{
+ g_assert(item != nullptr );
+
+ if (this->active_shape != item) {
+ // The active shape has changed
+ // Rebuild everything
+ this->active_shape = item;
+ // Remove existing active shape listeners
+ if (this->active_shape_repr) {
+ sp_repr_remove_listener_by_data(this->active_shape_repr, this);
+ Inkscape::GC::release(this->active_shape_repr);
+
+ sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this);
+ Inkscape::GC::release(this->active_shape_layer_repr);
+ }
+
+ // Listen in case the active shape changes
+ this->active_shape_repr = item->getRepr();
+ if (this->active_shape_repr) {
+ Inkscape::GC::anchor(this->active_shape_repr);
+ sp_repr_add_listener(this->active_shape_repr, &shape_repr_events, this);
+
+ this->active_shape_layer_repr = this->active_shape_repr->parent();
+ Inkscape::GC::anchor(this->active_shape_layer_repr);
+ sp_repr_add_listener(this->active_shape_layer_repr, &layer_repr_events, this);
+ }
+
+ cc_clear_active_knots(this->knots);
+
+ // The idea here is to try and add a group's children to solidify
+ // connection handling. We react to path objects with only one node.
+ for (auto& child: item->children) {
+ if (SP_IS_PATH(&child) && SP_PATH(&child)->nodesInPath() == 1) {
+ this->_activeShapeAddKnot((SPItem *) &child);
+ }
+ }
+ this->_activeShapeAddKnot(item);
+
+ } else {
+ // Ensure the item's connection_points map
+ // has been updated
+ item->document->ensureUpToDate();
+ }
+}
+
+void ConnectorTool::cc_set_active_conn(SPItem *item)
+{
+ g_assert( SP_IS_PATH(item) );
+
+ const SPCurve *curve = SP_PATH(item)->getCurveForEdit(true);
+ Geom::Affine i2dt = item->i2dt_affine();
+
+ if (this->active_conn == item) {
+ if (curve->is_empty()) {
+ // Connector is invisible because it is clipped to the boundary of
+ // two overlapping shapes.
+ this->endpt_handle[0]->hide();
+ this->endpt_handle[1]->hide();
+ } else {
+ // Just adjust handle positions.
+ Geom::Point startpt = *(curve->first_point()) * i2dt;
+ this->endpt_handle[0]->setPosition(startpt, 0);
+
+ Geom::Point endpt = *(curve->last_point()) * i2dt;
+ this->endpt_handle[1]->setPosition(endpt, 0);
+ }
+
+ return;
+ }
+
+ this->active_conn = item;
+
+ // Remove existing active conn listeners
+ if (this->active_conn_repr) {
+ sp_repr_remove_listener_by_data(this->active_conn_repr, this);
+ Inkscape::GC::release(this->active_conn_repr);
+ this->active_conn_repr = nullptr;
+ }
+
+ // Listen in case the active conn changes
+ this->active_conn_repr = item->getRepr();
+ if (this->active_conn_repr) {
+ Inkscape::GC::anchor(this->active_conn_repr);
+ sp_repr_add_listener(this->active_conn_repr, &shape_repr_events, this);
+ }
+
+ for (int i = 0; i < 2; ++i) {
+ // Create the handle if it doesn't exist
+ if ( this->endpt_handle[i] == nullptr ) {
+ SPKnot *knot = new SPKnot(this->desktop,
+ _("<b>Connector endpoint</b>: drag to reroute or connect to new shapes"));
+
+ knot->setShape(SP_KNOT_SHAPE_SQUARE);
+ knot->setSize(7);
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff);
+ knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff);
+ knot->updateCtrl();
+
+ // We don't want to use the standard knot handler,
+ // since we don't want this knot to be draggable.
+ g_signal_handler_disconnect(G_OBJECT(knot->item),
+ knot->_event_handler_id);
+
+ knot->_event_handler_id = 0;
+
+ g_signal_connect(G_OBJECT(knot->item), "event",
+ G_CALLBACK(cc_generic_knot_handler), knot);
+
+ this->endpt_handle[i] = knot;
+ }
+
+ // Remove any existing handlers
+ if (this->endpt_handler_id[i]) {
+ g_signal_handlers_disconnect_by_func(
+ G_OBJECT(this->endpt_handle[i]->item),
+ (void*)G_CALLBACK(endpt_handler), (gpointer) this );
+
+ this->endpt_handler_id[i] = 0;
+ }
+
+ // Setup handlers for connector endpoints, this is
+ // is as 'after' so that cc_generic_knot_handler is
+ // triggered first for any endpoint.
+ this->endpt_handler_id[i] = g_signal_connect_after(
+ G_OBJECT(this->endpt_handle[i]->item), "event",
+ G_CALLBACK(endpt_handler), this);
+ }
+
+ if (curve->is_empty()) {
+ // Connector is invisible because it is clipped to the boundary
+ // of two overlpapping shapes. So, it doesn't need endpoints.
+ return;
+ }
+
+ Geom::Point startpt = *(curve->first_point()) * i2dt;
+ this->endpt_handle[0]->setPosition(startpt, 0);
+
+ Geom::Point endpt = *(curve->last_point()) * i2dt;
+ this->endpt_handle[1]->setPosition(endpt, 0);
+
+ this->endpt_handle[0]->show();
+ this->endpt_handle[1]->show();
+}
+
+void cc_create_connection_point(ConnectorTool* cc)
+{
+ if (cc->active_shape && cc->state == SP_CONNECTOR_CONTEXT_IDLE) {
+ if (cc->selected_handle) {
+ cc_deselect_handle( cc->selected_handle );
+ }
+
+ SPKnot *knot = new SPKnot(cc->desktop, nullptr);
+
+ // We do not process events on this knot.
+ g_signal_handler_disconnect(G_OBJECT(knot->item),
+ knot->_event_handler_id);
+
+ knot->_event_handler_id = 0;
+
+ cc_select_handle( knot );
+ cc->selected_handle = knot;
+ cc->selected_handle->show();
+ cc->state = SP_CONNECTOR_CONTEXT_NEWCONNPOINT;
+ }
+}
+
+static bool cc_item_is_shape(SPItem *item)
+{
+ if (SP_IS_PATH(item)) {
+ const SPCurve * curve = (SP_SHAPE(item))->_curve;
+ if ( curve && !(curve->is_closed()) ) {
+ // Open paths are connectors.
+ return false;
+ }
+ } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/connector/ignoretext", true)) {
+ // Don't count text as a shape we can connect connector to.
+ return false;
+ }
+ }
+ return true;
+}
+
+
+bool cc_item_is_connector(SPItem *item)
+{
+ if (SP_IS_PATH(item)) {
+ bool closed = SP_PATH(item)->getCurveForEdit(true)->is_closed();
+ if (SP_PATH(item)->connEndPair.isAutoRoutingConn() && !closed) {
+ // To be considered a connector, an object must be a non-closed
+ // path that is marked with a "inkscape:connector-type" attribute.
+ return true;
+ }
+ }
+ return false;
+}
+
+
+void cc_selection_set_avoid(bool const set_avoid)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop == nullptr) {
+ return;
+ }
+
+ SPDocument *document = desktop->getDocument();
+
+ Inkscape::Selection *selection = desktop->getSelection();
+
+
+ int changes = 0;
+
+ for (SPItem *item: selection->items()) {
+ char const *value = (set_avoid) ? "true" : nullptr;
+
+ if (cc_item_is_shape(item)) {
+ item->setAttribute("inkscape:connector-avoid", value);
+ item->getAvoidRef().handleSettingChange();
+ changes++;
+ }
+ }
+
+ if (changes == 0) {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE,
+ _("Select <b>at least one non-connector object</b>."));
+ return;
+ }
+
+ char *event_desc = (set_avoid) ?
+ _("Make connectors avoid selected objects") :
+ _("Make connectors ignore selected objects");
+ DocumentUndo::done(document, SP_VERB_CONTEXT_CONNECTOR, event_desc);
+}
+
+void ConnectorTool::_selectionChanged(Inkscape::Selection *selection)
+{
+ SPItem *item = selection->singleItem();
+
+ if (this->active_conn == item) {
+ // Nothing to change.
+ return;
+ }
+
+ if (item == nullptr) {
+ this->cc_clear_active_conn();
+ return;
+ }
+
+ if (cc_item_is_connector(item)) {
+ this->cc_set_active_conn(item);
+ }
+}
+
+static void shape_event_attr_deleted(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node *child,
+ Inkscape::XML::Node */*ref*/, gpointer data)
+{
+ g_assert(data);
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data);
+
+ if (child == cc->active_shape_repr) {
+ // The active shape has been deleted. Clear active shape.
+ cc->cc_clear_active_shape();
+ }
+}
+
+
+static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const */*old_value*/, gchar const */*new_value*/, bool /*is_interactive*/, gpointer data)
+{
+ g_assert(data);
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data);
+
+ // Look for changes that result in onscreen movement.
+ if (!strcmp(name, "d") || !strcmp(name, "x") || !strcmp(name, "y") ||
+ !strcmp(name, "width") || !strcmp(name, "height") ||
+ !strcmp(name, "transform")) {
+ if (repr == cc->active_shape_repr) {
+ // Active shape has moved. Clear active shape.
+ cc->cc_clear_active_shape();
+ } else if (repr == cc->active_conn_repr) {
+ // The active conn has been moved.
+ // Set it again, which just sets new handle positions.
+ cc->cc_set_active_conn(cc->active_conn);
+ }
+ }
+}
+
+}
+}
+}
+
+
+/*
+ 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/tools/connector-tool.h b/src/ui/tools/connector-tool.h
new file mode 100644
index 0000000..c71e154
--- /dev/null
+++ b/src/ui/tools/connector-tool.h
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CONNECTOR_CONTEXT_H
+#define SEEN_CONNECTOR_CONTEXT_H
+
+/*
+ * Connector creation tool
+ *
+ * Authors:
+ * Michael Wybrow <mjwybrow@users.sourceforge.net>
+ *
+ * Copyright (C) 2005 Michael Wybrow
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <map>
+#include <string>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+
+class SPItem;
+class SPCurve;
+class SPKnot;
+struct SPCanvasItem;
+
+namespace Avoid {
+ class ConnRef;
+}
+
+namespace Inkscape {
+ class Selection;
+
+ namespace XML {
+ class Node;
+ }
+}
+
+#define SP_CONNECTOR_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ConnectorTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+//#define SP_IS_CONNECTOR_CONTEXT(obj) (dynamic_cast<const ConnectorTool*>((const ToolBase*)obj) != NULL)
+
+enum {
+ SP_CONNECTOR_CONTEXT_IDLE,
+ SP_CONNECTOR_CONTEXT_DRAGGING,
+ SP_CONNECTOR_CONTEXT_CLOSE,
+ SP_CONNECTOR_CONTEXT_STOP,
+ SP_CONNECTOR_CONTEXT_REROUTING,
+ SP_CONNECTOR_CONTEXT_NEWCONNPOINT
+};
+
+typedef std::map<SPKnot *, int> SPKnotList;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ConnectorTool : public ToolBase {
+public:
+ ConnectorTool();
+ ~ConnectorTool() override;
+
+ Inkscape::Selection *selection;
+ Geom::Point p[5];
+
+ /** \invar npoints in {0, 2}. */
+ gint npoints;
+ unsigned int state : 4;
+
+ // Red curve
+ SPCanvasItem *red_bpath;
+ SPCurve *red_curve;
+ guint32 red_color;
+
+ // Green curve
+ SPCurve *green_curve;
+
+ // The new connector
+ SPItem *newconn;
+ Avoid::ConnRef *newConnRef;
+ gdouble curvature;
+ bool isOrthogonal;
+
+ // The active shape
+ SPItem *active_shape;
+ Inkscape::XML::Node *active_shape_repr;
+ Inkscape::XML::Node *active_shape_layer_repr;
+
+ // Same as above, but for the active connector
+ SPItem *active_conn;
+ Inkscape::XML::Node *active_conn_repr;
+ sigc::connection sel_changed_connection;
+
+ // The activehandle
+ SPKnot *active_handle;
+
+ // The selected handle, used in editing mode
+ SPKnot *selected_handle;
+
+ SPItem *clickeditem;
+ SPKnot *clickedhandle;
+
+ SPKnotList knots;
+ SPKnot *endpt_handle[2];
+ guint endpt_handler_id[2];
+ gchar *shref;
+ gchar *ehref;
+ SPCanvasItem *c0, *c1, *cl0, *cl1;
+
+ static std::string const prefsPath;
+
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ std::string const& getPrefsPath() override;
+
+ void cc_clear_active_shape();
+ void cc_set_active_conn(SPItem *item);
+ void cc_clear_active_conn();
+
+private:
+ void _selectionChanged(Inkscape::Selection *selection);
+
+ bool _handleButtonPress(GdkEventButton const &bevent);
+ bool _handleMotionNotify(GdkEventMotion const &mevent);
+ bool _handleButtonRelease(GdkEventButton const &revent);
+ bool _handleKeyPress(guint const keyval);
+
+ void _setInitialPoint(Geom::Point const p);
+ void _setSubsequentPoint(Geom::Point const p);
+ void _finishSegment(Geom::Point p);
+ void _resetColors();
+ void _finish();
+ void _concatColorsAndFlush();
+ void _flushWhite(SPCurve *gc);
+
+ void _activeShapeAddKnot(SPItem* item);
+ void _setActiveShape(SPItem *item);
+ bool _ptHandleTest(Geom::Point& p, gchar **href);
+
+ void _reroutingFinish(Geom::Point *const p);
+};
+
+void cc_selection_set_avoid(bool const set_ignore);
+void cc_create_connection_point(ConnectorTool* cc);
+void cc_remove_connection_point(ConnectorTool* cc);
+bool cc_item_is_connector(SPItem *item);
+
+}
+}
+}
+
+#endif /* !SEEN_CONNECTOR_CONTEXT_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/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp
new file mode 100644
index 0000000..2552944
--- /dev/null
+++ b/src/ui/tools/dropper-tool.cpp
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Tool for picking colors from drawing
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <2geom/transforms.h>
+#include <2geom/circle.h>
+
+#include "color-rgba.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "message-context.h"
+#include "preferences.h"
+#include "selection.h"
+#include "sp-cursor.h"
+#include "verbs.h"
+
+#include "display/canvas-bpath.h"
+#include "display/canvas-arena.h"
+#include "display/curve.h"
+#include "display/cairo-utils.h"
+
+#include "include/macros.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/pixmaps/cursor-dropper-f.xpm"
+#include "ui/pixmaps/cursor-dropper-s.xpm"
+#include "ui/pixmaps/cursor-dropping-f.xpm"
+#include "ui/pixmaps/cursor-dropping-s.xpm"
+
+#include "svg/svg-color.h"
+
+#include "ui/tools/dropper-tool.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& DropperTool::getPrefsPath() {
+ return DropperTool::prefsPath;
+}
+
+const std::string DropperTool::prefsPath = "/tools/dropper";
+
+DropperTool::DropperTool()
+ : ToolBase(cursor_dropper_f_xpm)
+ , R(0)
+ , G(0)
+ , B(0)
+ , alpha(0)
+ , radius(0)
+ , invert(false)
+ , stroke(false)
+ , dropping(false)
+ , dragging(false)
+ , grabbed(nullptr)
+ , area(nullptr)
+ , centre(0, 0)
+{
+}
+
+DropperTool::~DropperTool() = default;
+
+void DropperTool::setup() {
+ ToolBase::setup();
+
+ /* TODO: have a look at CalligraphicTool::setup where the same is done.. generalize? */
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1));
+
+ SPCurve *c = new SPCurve(path);
+
+ this->area = sp_canvas_bpath_new(this->desktop->getControls(), c);
+
+ c->unref();
+
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->area), 0x00000000,(SPWindRule)0);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_hide(this->area);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/dropper/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/dropper/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void DropperTool::finish() {
+ this->enableGrDrag(false);
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ if (this->area) {
+ sp_canvas_item_destroy(this->area);
+ this->area = nullptr;
+ }
+
+ ToolBase::finish();
+}
+
+/**
+ * Returns the current dropper context color.
+ */
+guint32 DropperTool::get_color(bool invert) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE);
+ bool setalpha = prefs->getBool("/tools/dropper/setalpha", true);
+
+ return SP_RGBA32_F_COMPOSE(
+ fabs(invert - this->R),
+ fabs(invert - this->G),
+ fabs(invert - this->B),
+ (pick == SP_DROPPER_PICK_ACTUAL && setalpha) ? this->alpha : 1.0);
+}
+
+bool DropperTool::root_handler(GdkEvent* event) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ int ret = FALSE;
+ int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE);
+
+ // Decide first what kind of 'mode' we're in.
+ if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) {
+ switch (event->key.keyval) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->stroke = event->type == GDK_KEY_PRESS;
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ this->dropping = event->type == GDK_KEY_PRESS;
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ this->invert = event->type == GDK_KEY_PRESS;
+ break;
+ }
+ }
+
+ // Get color from selected object instead.
+ if(this->dropping) {
+ Inkscape::Selection *selection = desktop->getSelection();
+ g_assert(selection);
+ guint32 apply_color;
+ bool apply_set = false;
+ for (auto& obj: selection->objects()) {
+ if(obj->style) {
+ double opacity = 1.0;
+ if(!this->stroke && obj->style->fill.set) {
+ if(obj->style->fill_opacity.set) {
+ opacity = obj->style->fill_opacity.value;
+ }
+ apply_color = obj->style->fill.value.color.toRGBA32(opacity);
+ apply_set = true;
+ } else if(this->stroke && obj->style->stroke.set) {
+ if(obj->style->stroke_opacity.set) {
+ opacity = obj->style->stroke_opacity.value;
+ }
+ apply_color = obj->style->stroke.value.color.toRGBA32(opacity);
+ apply_set = true;
+ }
+ }
+ }
+ if(apply_set) {
+ this->R = SP_RGBA32_R_F(apply_color);
+ this->G = SP_RGBA32_G_F(apply_color);
+ this->B = SP_RGBA32_B_F(apply_color);
+ this->alpha = SP_RGBA32_A_F(apply_color);
+ } else {
+ // This means that having no selection or some other error
+ // we will default back to normal dropper mode.
+ this->dropping = false;
+ }
+ }
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ this->centre = Geom::Point(event->button.x, event->button.y);
+ this->dragging = true;
+ ret = TRUE;
+ }
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK,
+ nullptr, event->button.time);
+ this->grabbed = SP_CANVAS_ITEM(desktop->acetate);
+
+ break;
+ case GDK_MOTION_NOTIFY:
+ if (event->motion.state & GDK_BUTTON2_MASK || event->motion.state & GDK_BUTTON3_MASK) {
+ // pass on middle and right drag
+ ret = FALSE;
+ break;
+ } else if (!this->space_panning) {
+ // otherwise, constantly calculate color no matter is any button pressed or not
+ double rw = 0.0;
+ double R(0), G(0), B(0), A(0);
+
+ if (this->dragging) {
+ // calculate average
+
+ // radius
+ rw = std::min(Geom::L2(Geom::Point(event->button.x, event->button.y) - this->centre), 400.0);
+ if (rw == 0) { // happens sometimes, little idea why...
+ break;
+ }
+ this->radius = rw;
+
+ Geom::Point const cd = desktop->w2d(this->centre);
+ Geom::Affine const w2dt = desktop->w2d();
+ const double scale = rw * w2dt.descrim();
+ Geom::Affine const sm( Geom::Scale(scale, scale) * Geom::Translate(cd) );
+ sp_canvas_item_affine_absolute(this->area, sm);
+ sp_canvas_item_show(this->area);
+
+ /* Get buffer */
+ Geom::Rect r(this->centre, this->centre);
+ r.expandBy(rw);
+ if (!r.hasZeroArea()) {
+ Geom::IntRect area = r.roundOutwards();
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area.width(), area.height());
+ sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area);
+ ink_cairo_surface_average_color_premul(s, R, G, B, A);
+ cairo_surface_destroy(s);
+ }
+ } else {
+ // pick single pixel
+ Geom::IntRect area = Geom::IntRect::from_xywh(floor(event->button.x), floor(event->button.y), 1, 1);
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
+ sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area);
+ ink_cairo_surface_average_color_premul(s, R, G, B, A);
+ cairo_surface_destroy(s);
+ }
+
+ if (pick == SP_DROPPER_PICK_VISIBLE) {
+ // compose with page color
+ guint32 bg = desktop->getNamedView()->pagecolor;
+ R = R + (SP_RGBA32_R_F(bg)) * (1 - A);
+ G = G + (SP_RGBA32_G_F(bg)) * (1 - A);
+ B = B + (SP_RGBA32_B_F(bg)) * (1 - A);
+ A = 1.0;
+ } else {
+ // un-premultiply color channels
+ if (A > 0) {
+ R /= A;
+ G /= A;
+ B /= A;
+ }
+ }
+
+ if (fabs(A) < 1e-4) {
+ A = 0; // suppress exponentials, CSS does not allow that
+ }
+
+ // remember color
+ if(!this->dropping && (R != this->R || G != this->G || B != this->B || A != this->alpha)) {
+ this->R = R;
+ this->G = G;
+ this->B = B;
+ this->alpha = A;
+ }
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && !this->space_panning) {
+ sp_canvas_item_hide(this->area);
+ this->dragging = false;
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ g_assert(selection);
+ std::vector<SPItem *> old_selection(selection->items().begin(), selection->items().end());
+ if(this->dropping) {
+ Geom::Point const button_w(event->button.x, event->button.y);
+ // remember clicked item, disregarding groups, honoring Alt
+ this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+
+ // Change selected object to object under cursor
+ if (this->item_to_select) {
+ std::vector<SPItem *> vec(selection->items().begin(), selection->items().end());
+ selection->set(this->item_to_select);
+ }
+ } else {
+ if (prefs->getBool("/tools/dropper/onetimepick", false)) {
+ // "One time" pick from Fill/Stroke dialog stroke page, always apply fill or stroke (ignore <Shift> key)
+ stroke = (prefs->getInt("/dialogs/fillstroke/page", 0) == 0) ? false : true;
+ }
+ }
+
+ // do the actual color setting
+ sp_desktop_set_color(desktop, ColorRGBA(this->get_color(this->invert)), false, !this->stroke);
+
+ // REJON: set aux. toolbar input to hex color!
+ if (!(desktop->getSelection()->isEmpty())) {
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_DROPPER,
+ _("Set picked color"));
+ }
+ if(this->dropping) {
+ selection->setList(old_selection);
+ }
+
+ if (prefs->getBool("/tools/dropper/onetimepick", false)) {
+ prefs->setBool("/tools/dropper/onetimepick", false);
+ sp_toggle_dropper(desktop);
+
+ // sp_toggle_dropper will delete ourselves.
+ // Thus, make sure we return immediately.
+ return true;
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // prevent the zoom field from activation
+ if (!MOD__CTRL_ONLY(event)) {
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Escape:
+ desktop->getSelection()->clear();
+ break;
+ }
+ break;
+ }
+
+ // set the status message to the right text.
+ gchar c[64];
+ sp_svg_write_color(c, sizeof(c), this->get_color(this->invert));
+
+ // alpha of color under cursor, to show in the statusbar
+ // locale-sensitive printf is OK, since this goes to the UI, not into SVG
+ gchar *alpha = g_strdup_printf(_(" alpha %.3g"), this->alpha);
+ // where the color is picked, to show in the statusbar
+ gchar *where = this->dragging ? g_strdup_printf(_(", averaged with radius %d"), (int) this->radius) : g_strdup_printf("%s", _(" under cursor"));
+ // message, to show in the statusbar
+ const gchar *message = this->dragging ? _("<b>Release mouse</b> to set color.") : _("<b>Click</b> to set fill, <b>Shift+click</b> to set stroke; <b>drag</b> to average color in area; with <b>Alt</b> to pick inverse color; <b>Ctrl+C</b> to copy the color under mouse to clipboard");
+
+ this->defaultMessageContext()->setF(
+ Inkscape::NORMAL_MESSAGE,
+ "<b>%s%s</b>%s. %s", c,
+ (pick == SP_DROPPER_PICK_VISIBLE) ? "" : alpha, where, message);
+
+ g_free(where);
+ g_free(alpha);
+
+ // Set the right cursor for the mode and apply the special Fill color
+ auto xpm = (this->dropping ? (this->stroke ? cursor_dropping_s_xpm : cursor_dropping_f_xpm) :
+ (this->stroke ? cursor_dropper_s_xpm : cursor_dropper_f_xpm));
+ GdkCursor *cursor = sp_cursor_from_xpm(xpm, this->get_color(this->invert));
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(desktop->getCanvas()));
+ gdk_window_set_cursor(window, cursor);
+ g_object_unref(cursor);
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+}
+}
+}
+
+
+/*
+ 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/tools/dropper-tool.h b/src/ui/tools/dropper-tool.h
new file mode 100644
index 0000000..d52049c
--- /dev/null
+++ b/src/ui/tools/dropper-tool.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_DROPPER_CONTEXT_H__
+#define __SP_DROPPER_CONTEXT_H__
+
+/*
+ * Tool for picking colors from drawing
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+
+#include "ui/tools/tool-base.h"
+
+struct SPCanvasItem;
+
+#define SP_DROPPER_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::DropperTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_DROPPER_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::DropperTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+enum {
+ SP_DROPPER_PICK_VISIBLE,
+ SP_DROPPER_PICK_ACTUAL
+};
+enum {
+ DONT_REDRAW_CURSOR,
+ DRAW_FILL_CURSOR,
+ DRAW_STROKE_CURSOR
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class DropperTool : public ToolBase {
+public:
+ DropperTool();
+ ~DropperTool() override;
+
+ static const std::string prefsPath;
+
+ const std::string& getPrefsPath() override;
+
+ guint32 get_color(bool invert=false);
+
+protected:
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+
+private:
+ double R;
+ double G;
+ double B;
+ double alpha;
+
+ double radius;
+ bool invert;
+ bool stroke;
+ bool dropping;
+ bool dragging;
+
+ SPCanvasItem* grabbed;
+ SPCanvasItem* area;
+ Geom::Point centre;
+};
+
+}
+}
+}
+
+#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/tools/dynamic-base.cpp b/src/ui/tools/dynamic-base.cpp
new file mode 100644
index 0000000..92eba3f
--- /dev/null
+++ b/src/ui/tools/dynamic-base.cpp
@@ -0,0 +1,166 @@
+// 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 "ui/tools/dynamic-base.h"
+
+#include "message-context.h"
+#include "display/sp-canvas-item.h"
+#include "desktop.h"
+#include "display/curve.h"
+
+#define MIN_PRESSURE 0.0
+#define MAX_PRESSURE 1.0
+#define DEFAULT_PRESSURE 1.0
+
+#define DRAG_MIN 0.0
+#define DRAG_DEFAULT 1.0
+#define DRAG_MAX 1.0
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+DynamicBase::DynamicBase(gchar const *const *cursor_shape)
+ : ToolBase(cursor_shape)
+ , accumulated(nullptr)
+ , currentshape(nullptr)
+ , currentcurve(nullptr)
+ , cal1(nullptr)
+ , cal2(nullptr)
+ , point1()
+ , point2()
+ , npoints(0)
+ , repr(nullptr)
+ , cur(0, 0)
+ , vel(0, 0)
+ , vel_max(0)
+ , acc(0, 0)
+ , ang(0, 0)
+ , last(0, 0)
+ , del(0, 0)
+ , pressure(DEFAULT_PRESSURE)
+ , xtilt(0)
+ , ytilt(0)
+ , dragging(false)
+ , usepressure(false)
+ , usetilt(false)
+ , mass(0.3)
+ , drag(DRAG_DEFAULT)
+ , angle(30.0)
+ , width(0.2)
+ , vel_thin(0.1)
+ , flatness(0.9)
+ , tremor(0)
+ , cap_rounding(0)
+ , is_drawing(false)
+ , abs_width(false)
+{
+}
+
+DynamicBase::~DynamicBase() {
+ if (this->accumulated) {
+ this->accumulated = this->accumulated->unref();
+ this->accumulated = nullptr;
+ }
+
+ for (auto i:segments) {
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(i));
+ }
+ segments.clear();
+
+ if (this->currentcurve) {
+ this->currentcurve = this->currentcurve->unref();
+ this->currentcurve = nullptr;
+ }
+
+ if (this->cal1) {
+ this->cal1 = this->cal1->unref();
+ this->cal1 = nullptr;
+ }
+
+ if (this->cal2) {
+ this->cal2 = this->cal2->unref();
+ this->cal2 = nullptr;
+ }
+
+ if (this->currentshape) {
+ sp_canvas_item_destroy(this->currentshape);
+ this->currentshape = nullptr;
+ }
+}
+
+void DynamicBase::set(const Inkscape::Preferences::Entry& value) {
+ Glib::ustring path = value.getEntryName();
+
+ // ignore preset modifications
+ static Glib::ustring const presets_path = this->pref_observer->observed_path + "/preset";
+ Glib::ustring const &full_path = value.getPath();
+
+ if (full_path.compare(0, presets_path.size(), presets_path) == 0) {
+ return;
+ }
+
+ if (path == "mass") {
+ this->mass = 0.01 * CLAMP(value.getInt(10), 0, 100);
+ } else if (path == "wiggle") {
+ this->drag = CLAMP((1 - 0.01 * value.getInt()), DRAG_MIN, DRAG_MAX); // drag is inverse to wiggle
+ } else if (path == "angle") {
+ this->angle = CLAMP(value.getDouble(), -90, 90);
+ } else if (path == "width") {
+ this->width = 0.01 * CLAMP(value.getInt(10), 1, 100);
+ } else if (path == "thinning") {
+ this->vel_thin = 0.01 * CLAMP(value.getInt(10), -100, 100);
+ } else if (path == "tremor") {
+ this->tremor = 0.01 * CLAMP(value.getInt(), 0, 100);
+ } else if (path == "flatness") {
+ this->flatness = 0.01 * CLAMP(value.getInt(), 0, 100);
+ } else if (path == "usepressure") {
+ this->usepressure = value.getBool();
+ } else if (path == "usetilt") {
+ this->usetilt = value.getBool();
+ } else if (path == "abs_width") {
+ this->abs_width = value.getBool();
+ } else if (path == "cap_rounding") {
+ this->cap_rounding = value.getDouble();
+ }
+}
+
+/* Get normalized point */
+Geom::Point DynamicBase::getNormalizedPoint(Geom::Point v) const {
+ Geom::Rect drect = this->desktop->get_display_area();
+
+ double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] );
+
+ return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max);
+}
+
+/* Get view point */
+Geom::Point DynamicBase::getViewPoint(Geom::Point n) const {
+ Geom::Rect drect = this->desktop->get_display_area();
+
+ double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] );
+
+ return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]);
+}
+
+}
+}
+}
+
+/*
+ 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/tools/dynamic-base.h b/src/ui/tools/dynamic-base.h
new file mode 100644
index 0000000..1a36f30
--- /dev/null
+++ b/src/ui/tools/dynamic-base.h
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef COMMON_CONTEXT_H_SEEN
+#define COMMON_CONTEXT_H_SEEN
+
+/*
+ * Common drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2008 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+#include "display/sp-canvas-item.h"
+
+struct SPCanvasItem;
+class SPCurve;
+
+namespace Inkscape {
+ namespace XML {
+ class Node;
+ }
+}
+
+#define SAMPLING_SIZE 8 /* fixme: ?? */
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class DynamicBase : public ToolBase {
+public:
+ DynamicBase(gchar const *const *cursor_shape);
+ ~DynamicBase() override;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+
+protected:
+ /** accumulated shape which ultimately goes in svg:path */
+ SPCurve *accumulated;
+
+ /** canvas items for "committed" segments */
+ std::vector<SPCanvasItem*> segments;
+
+ /** canvas item for red "leading" segment */
+ SPCanvasItem *currentshape;
+ /** shape of red "leading" segment */
+ SPCurve *currentcurve;
+
+ /** left edge of the stroke; combined to get accumulated */
+ SPCurve *cal1;
+
+ /** right edge of the stroke; combined to get accumulated */
+ SPCurve *cal2;
+
+ /** left edge points for this segment */
+ Geom::Point point1[SAMPLING_SIZE];
+
+ /** right edge points for this segment */
+ Geom::Point point2[SAMPLING_SIZE];
+
+ /** number of edge points for this segment */
+ gint npoints;
+
+ /* repr */
+ Inkscape::XML::Node *repr;
+
+ /* common */
+ Geom::Point cur;
+ Geom::Point vel;
+ double vel_max;
+ Geom::Point acc;
+ Geom::Point ang;
+ Geom::Point last;
+ Geom::Point del;
+
+ /* extended input data */
+ gdouble pressure;
+ gdouble xtilt;
+ gdouble ytilt;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressure;
+ bool usetilt;
+ double mass, drag;
+ double angle;
+ double width;
+
+ double vel_thin;
+ double flatness;
+ double tremor;
+ double cap_rounding;
+
+ bool is_drawing;
+
+ /** uses absolute width independent of zoom */
+ bool abs_width;
+
+ Geom::Point getViewPoint(Geom::Point n) const;
+ Geom::Point getNormalizedPoint(Geom::Point v) const;
+};
+
+}
+}
+}
+
+#endif // COMMON_CONTEXT_H_SEEN
+
+/*
+ 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/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp
new file mode 100644
index 0000000..8a98350
--- /dev/null
+++ b/src/ui/tools/eraser-tool.cpp
@@ -0,0 +1,1119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Eraser drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * MenTaLguY <mental@rydia.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2005-2007 bulia byak
+ * Copyright (C) 2006 MenTaLguY
+ * Copyright (C) 2008 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noERASER_VERBOSE
+
+#include <string>
+#include <cstring>
+#include <numeric>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/bezier-utils.h>
+#include <2geom/pathvector.h>
+
+#include "context-fns.h"
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "layer-manager.h"
+#include "layer-model.h"
+#include "message-context.h"
+#include "path-chemistry.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "splivarot.h"
+#include "verbs.h"
+
+#include "display/sp-canvas.h"
+#include "display/canvas-arena.h"
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+
+#include "include/macros.h"
+
+#include "object/sp-clippath.h"
+#include "object/sp-item-group.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+#include "object/sp-use.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-eraser.xpm"
+
+#include "svg/svg.h"
+
+#include "ui/tools/eraser-tool.h"
+
+using Inkscape::DocumentUndo;
+
+#define ERC_RED_RGBA 0xff0000ff
+
+#define TOLERANCE_ERASER 0.1
+
+#define ERASER_EPSILON 0.5e-6
+#define ERASER_EPSILON_START 0.5e-2
+#define ERASER_VEL_START 1e-5
+
+#define DRAG_MIN 0.0
+#define DRAG_DEFAULT 1.0
+#define DRAG_MAX 1.0
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& EraserTool::getPrefsPath() {
+ return EraserTool::prefsPath;
+}
+
+const std::string EraserTool::prefsPath = "/tools/eraser";
+
+EraserTool::EraserTool()
+ : DynamicBase(cursor_eraser_xpm)
+ , nowidth(false)
+{
+}
+
+EraserTool::~EraserTool() = default;
+
+void EraserTool::setup() {
+ DynamicBase::setup();
+
+ this->accumulated = new SPCurve();
+ this->currentcurve = new SPCurve();
+
+ this->cal1 = new SPCurve();
+ this->cal2 = new SPCurve();
+
+ this->currentshape = sp_canvas_item_new(desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr);
+
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), ERC_RED_RGBA, SP_WIND_RULE_EVENODD);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), desktop);
+
+/*
+static ProfileFloatElement f_profile[PROFILE_FLOAT_SIZE] = {
+ {"mass",0.02, 0.0, 1.0},
+ {"wiggle",0.0, 0.0, 1.0},
+ {"angle",30.0, -90.0, 90.0},
+ {"thinning",0.1, -1.0, 1.0},
+ {"tremor",0.0, 0.0, 1.0},
+ {"flatness",0.9, 0.0, 1.0},
+ {"cap_rounding",0.0, 0.0, 5.0}
+};
+*/
+
+ sp_event_context_read(this, "mass");
+ sp_event_context_read(this, "wiggle");
+ sp_event_context_read(this, "angle");
+ sp_event_context_read(this, "width");
+ sp_event_context_read(this, "thinning");
+ sp_event_context_read(this, "tremor");
+ sp_event_context_read(this, "flatness");
+ sp_event_context_read(this, "tracebackground");
+ sp_event_context_read(this, "usepressure");
+ sp_event_context_read(this, "usetilt");
+ sp_event_context_read(this, "abs_width");
+ sp_event_context_read(this, "cap_rounding");
+
+ this->is_drawing = false;
+ //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/eraser/selcue", false) != 0) {
+ this->enableSelectionCue();
+ }
+
+ // TODO temp force:
+ this->enableSelectionCue();
+}
+
+static double
+flerp(double f0, double f1, double p)
+{
+ return f0 + ( f1 - f0 ) * p;
+}
+
+void EraserTool::reset(Geom::Point p) {
+ this->last = this->cur = getNormalizedPoint(p);
+ this->vel = Geom::Point(0,0);
+ this->vel_max = 0;
+ this->acc = Geom::Point(0,0);
+ this->ang = Geom::Point(0,0);
+ this->del = Geom::Point(0,0);
+}
+
+void EraserTool::extinput(GdkEvent *event) {
+ if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure))
+ this->pressure = CLAMP (this->pressure, ERC_MIN_PRESSURE, ERC_MAX_PRESSURE);
+ else
+ this->pressure = ERC_DEFAULT_PRESSURE;
+
+ if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt))
+ this->xtilt = CLAMP (this->xtilt, ERC_MIN_TILT, ERC_MAX_TILT);
+ else
+ this->xtilt = ERC_DEFAULT_TILT;
+
+ if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt))
+ this->ytilt = CLAMP (this->ytilt, ERC_MIN_TILT, ERC_MAX_TILT);
+ else
+ this->ytilt = ERC_DEFAULT_TILT;
+}
+
+
+bool EraserTool::apply(Geom::Point p) {
+ Geom::Point n = getNormalizedPoint(p);
+
+ /* Calculate mass and drag */
+ double const mass = flerp(1.0, 160.0, this->mass);
+ double const drag = flerp(0.0, 0.5, this->drag * this->drag);
+
+ /* Calculate force and acceleration */
+ Geom::Point force = n - this->cur;
+
+ // If force is below the absolute threshold ERASER_EPSILON,
+ // or we haven't yet reached ERASER_VEL_START (i.e. at the beginning of stroke)
+ // _and_ the force is below the (higher) ERASER_EPSILON_START threshold,
+ // discard this move.
+ // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen,
+ // especially bothersome at the start of the stroke where we don't yet have the inertia to
+ // smooth them out.
+ if ( Geom::L2(force) < ERASER_EPSILON || (this->vel_max < ERASER_VEL_START && Geom::L2(force) < ERASER_EPSILON_START)) {
+ return FALSE;
+ }
+
+ this->acc = force / mass;
+
+ /* Calculate new velocity */
+ this->vel += this->acc;
+
+ if (Geom::L2(this->vel) > this->vel_max)
+ this->vel_max = Geom::L2(this->vel);
+
+ /* Calculate angle of drawing tool */
+
+ double a1;
+ if (this->usetilt) {
+ // 1a. calculate nib angle from input device tilt:
+ gdouble length = std::sqrt(this->xtilt*this->xtilt + this->ytilt*this->ytilt);;
+
+ if (length > 0) {
+ Geom::Point ang1 = Geom::Point(this->ytilt/length, this->xtilt/length);
+ a1 = atan2(ang1);
+ }
+ else
+ a1 = 0.0;
+ }
+ else {
+ // 1b. fixed dc->angle (absolutely flat nib):
+ double const radians = ( (this->angle - 90) / 180.0 ) * M_PI;
+ Geom::Point ang1 = Geom::Point(-sin(radians), cos(radians));
+ a1 = atan2(ang1);
+ }
+
+ // 2. perpendicular to dc->vel (absolutely non-flat nib):
+ gdouble const mag_vel = Geom::L2(this->vel);
+ if ( mag_vel < ERASER_EPSILON ) {
+ return FALSE;
+ }
+ Geom::Point ang2 = Geom::rot90(this->vel) / mag_vel;
+
+ // 3. Average them using flatness parameter:
+ // calculate angles
+ double a2 = atan2(ang2);
+ // flip a2 to force it to be in the same half-circle as a1
+ bool flipped = false;
+ if (fabs (a2-a1) > 0.5*M_PI) {
+ a2 += M_PI;
+ flipped = true;
+ }
+ // normalize a2
+ if (a2 > M_PI)
+ a2 -= 2*M_PI;
+ if (a2 < -M_PI)
+ a2 += 2*M_PI;
+ // find the flatness-weighted bisector angle, unflip if a2 was flipped
+ // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this?
+ double new_ang = a1 + (1 - this->flatness) * (a2 - a1) - (flipped? M_PI : 0);
+
+ // Try to detect a sudden flip when the new angle differs too much from the previous for the
+ // current velocity; in that case discard this move
+ double angle_delta = Geom::L2(Geom::Point (cos (new_ang), sin (new_ang)) - this->ang);
+ if ( angle_delta / Geom::L2(this->vel) > 4000 ) {
+ return FALSE;
+ }
+
+ // convert to point
+ this->ang = Geom::Point (cos (new_ang), sin (new_ang));
+
+// g_print ("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(dc->acc), dc->vel_max, Geom::L2(dc->vel), a1, a2, new_ang);
+
+ /* Apply drag */
+ this->vel *= 1.0 - drag;
+
+ /* Update position */
+ this->last = this->cur;
+ this->cur += this->vel;
+
+ return TRUE;
+}
+
+void EraserTool::brush() {
+ g_assert( this->npoints >= 0 && this->npoints < SAMPLING_SIZE );
+
+ // How much velocity thins strokestyle
+ double vel_thin = flerp (0, 160, this->vel_thin);
+
+ // Influence of pressure on thickness
+ double pressure_thick = (this->usepressure ? this->pressure : 1.0);
+
+ // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass
+ // drag)
+ Geom::Point brush = getViewPoint(this->cur);
+ //Geom::Point brush_w = SP_EVENT_CONTEXT(dc)->desktop->d2w(brush);
+
+ double trace_thick = 1;
+
+ double width = (pressure_thick * trace_thick - vel_thin * Geom::L2(this->vel)) * this->width;
+
+ double tremble_left = 0, tremble_right = 0;
+ if (this->tremor > 0) {
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double x1, x2, w, y1, y2;
+ do {
+ x1 = 2.0 * g_random_double_range(0,1) - 1.0;
+ x2 = 2.0 * g_random_double_range(0,1) - 1.0;
+ w = x1 * x1 + x2 * x2;
+ } while ( w >= 1.0 );
+ w = sqrt( (-2.0 * log( w ) ) / w );
+ y1 = x1 * w;
+ y2 = x2 * w;
+
+ // deflect both left and right edges randomly and independently, so that:
+ // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve;
+ // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths;
+ // (3) deflection somewhat depends on speed, to prevent fast strokes looking
+ // comparatively smooth and slow ones excessively jittery
+ tremble_left = (y1)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel));
+ tremble_right = (y2)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel));
+ }
+
+ if ( width < 0.02 * this->width ) {
+ width = 0.02 * this->width;
+ }
+
+ double dezoomify_factor = 0.05 * 1000;
+ if (!this->abs_width) {
+ dezoomify_factor /= SP_EVENT_CONTEXT(this)->desktop->current_zoom();
+ }
+
+ Geom::Point del_left = dezoomify_factor * (width + tremble_left) * this->ang;
+ Geom::Point del_right = dezoomify_factor * (width + tremble_right) * this->ang;
+
+ this->point1[this->npoints] = brush + del_left;
+ this->point2[this->npoints] = brush - del_right;
+
+ if (this->nowidth) {
+ this->point1[this->npoints] = Geom::middle_point(this->point1[this->npoints],this->point2[this->npoints]);
+ }
+ this->del = 0.5*(del_left + del_right);
+
+ this->npoints++;
+}
+
+static void
+sp_erc_update_toolbox (SPDesktop *desktop, const gchar *id, double value)
+{
+ desktop->setToolboxAdjustmentValue (id, value);
+}
+
+void EraserTool::cancel() {
+ SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop;
+ this->dragging = FALSE;
+ this->is_drawing = false;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ /* Remove all temporary line segments */
+ for (auto i : this->segments)
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(i));
+ this->segments.clear();
+ /* reset accumulated curve */
+ this->accumulated->reset();
+ this->clear_current();
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+}
+
+bool EraserTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint eraser_mode = prefs->getInt("/tools/eraser/mode", 2);
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+
+ this->reset(button_dt);
+ this->extinput(event);
+ this->apply(button_dt);
+
+ this->accumulated->reset();
+
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+ if ( eraser_mode == ERASER_MODE_DELETE ) {
+ Inkscape::Rubberband::get(desktop)->start(desktop, button_dt);
+ Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ }
+ /* initialize first point */
+ this->npoints = 0;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ ( GDK_KEY_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK ),
+ nullptr,
+ event->button.time);
+
+ ret = TRUE;
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(3);
+ this->is_drawing = true;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY: {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w)
+ );
+ this->extinput(event);
+
+ this->message_context->clear();
+
+ if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ this->dragging = TRUE;
+
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke"));
+
+ if (!this->apply(motion_dt)) {
+ ret = TRUE;
+ break;
+ }
+
+ if ( this->cur != this->last ) {
+ this->brush();
+ g_assert( this->npoints > 0 );
+ this->fit_and_split(false);
+ }
+
+ ret = TRUE;
+ }
+ if ( eraser_mode == ERASER_MODE_DELETE ) {
+ this->accumulated->reset();
+ Inkscape::Rubberband::get(desktop)->move(motion_dt);
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE: {
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ desktop->canvas->endForcedFullRedraws();
+ this->is_drawing = false;
+
+ if (this->dragging && event->button.button == 1 && !this->space_panning) {
+ this->dragging = FALSE;
+
+ this->apply(motion_dt);
+
+ /* Remove all temporary line segments */
+ for (auto i : this->segments)
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(i));
+ this->segments.clear();
+
+ /* Create object */
+ this->fit_and_split(true);
+ this->accumulate();
+ this->set_to_accumulated(); // performs document_done
+
+ /* reset accumulated curve */
+ this->accumulated->reset();
+
+ this->clear_current();
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+
+ this->message_context->clear();
+ ret = TRUE;
+ }
+
+ if (eraser_mode == ERASER_MODE_DELETE && Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+
+ break;
+ }
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+// case GDK_KEY_Up:
+// case GDK_KEY_KP_Up:
+// if (!MOD__CTRL_ONLY(event)) {
+// this->angle += 5.0;
+
+// if (this->angle > 90.0) {
+// this->angle = 90.0;
+// }
+// sp_erc_update_toolbox (desktop, "eraser-angle", this->angle);
+// ret = TRUE;
+// }
+// break;
+
+// case GDK_KEY_Down:
+// case GDK_KEY_KP_Down:
+// if (!MOD__CTRL_ONLY(event)) {
+// this->angle -= 5.0;
+
+// if (this->angle < -90.0) {
+// this->angle = -90.0;
+// }
+
+// sp_erc_update_toolbox (desktop, "eraser-angle", this->angle);
+// ret = TRUE;
+// }
+// break;
+
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width += 0.01;
+
+ if (this->width > 1.0) {
+ this->width = 1.0;
+ }
+
+ sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100); // the same spinbutton is for alt+x
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width -= 0.01;
+
+ if (this->width < 0.01) {
+ this->width = 0.01;
+ }
+
+ sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ this->width = 0.01;
+ sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100);
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ this->width = 1.0;
+ sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100);
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("eraser-width");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if ( eraser_mode == ERASER_MODE_DELETE ) {
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+ if (this->is_drawing) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (MOD__CTRL_ONLY(event) && this->is_drawing) {
+ // if drawing, cancel, otherwise pass it up for undo
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ this->message_context->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = DynamicBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void EraserTool::clear_current() {
+ // reset bpath
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), nullptr);
+
+ // reset curve
+ this->currentcurve->reset();
+ this->cal1->reset();
+ this->cal2->reset();
+
+ // reset points
+ this->npoints = 0;
+}
+
+void EraserTool::set_to_accumulated() {
+ bool workDone = false;
+ SPDocument *document = this->desktop->doc();
+ if (!this->accumulated->is_empty()) {
+ if (!this->repr) {
+ /* Create object */
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+
+ /* Set style */
+ sp_desktop_apply_style_tool (this->desktop, repr, "/tools/eraser", false);
+
+ this->repr = repr;
+ }
+ SPObject * top_layer = desktop->currentRoot();
+ SPItem *item_repr = SP_ITEM(top_layer->appendChildRepr(this->repr));
+ Inkscape::GC::release(this->repr);
+ item_repr->updateRepr();
+ Geom::PathVector pathv = this->accumulated->get_pathvector() * this->desktop->dt2doc();
+ pathv *= item_repr->i2doc_affine().inverse();
+ gchar *str = sp_svg_write_path(pathv);
+ g_assert( str != nullptr );
+ this->repr->setAttribute("d", str);
+ g_free(str);
+ Geom::OptRect eraserBbox;
+ if ( this->repr ) {
+ bool wasSelection = false;
+ Inkscape::Selection *selection = this->desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint eraser_mode = prefs->getInt("/tools/eraser/mode", ERASER_MODE_CLIP);
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+
+ SPItem* acid = SP_ITEM(this->desktop->doc()->getObjectByRepr(this->repr));
+ eraserBbox = acid->documentVisualBounds();
+ std::vector<SPItem*> remainingItems;
+ std::vector<SPItem*> toWorkOn;
+ if (selection->isEmpty()) {
+ if (eraser_mode == ERASER_MODE_CUT || eraser_mode == ERASER_MODE_CLIP) {
+ toWorkOn = document->getItemsPartiallyInBox(this->desktop->dkey, *eraserBbox, false, false, false, true);
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(this->desktop);
+ toWorkOn = document->getItemsAtPoints(this->desktop->dkey, r->getPoints());
+ }
+ toWorkOn.erase(std::remove(toWorkOn.begin(), toWorkOn.end(), acid), toWorkOn.end());
+ } else {
+ if (eraser_mode == ERASER_MODE_DELETE) {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(this->desktop);
+ std::vector<SPItem*> touched;
+ touched = document->getItemsAtPoints(this->desktop->dkey, r->getPoints());
+ for (auto i : touched) {
+ if(selection->includes(i)){
+ toWorkOn.push_back(i);
+ }
+ }
+ } else {
+ toWorkOn.insert(toWorkOn.end(), selection->items().begin(), selection->items().end());
+ }
+ wasSelection = true;
+ }
+
+ if ( !toWorkOn.empty() ) {
+ if (eraser_mode == ERASER_MODE_CUT) {
+ for (auto i : toWorkOn){
+ SPItem *item = i;
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if (SP_IS_PATH(item) && SP_PATH(item)->nodesInPath () == 2){
+ SPItem *item = i;
+ item->deleteObject(true);
+ workDone = true;
+ } else if (SP_IS_GROUP(item) || use ) {
+ /*Do nothing*/
+ } else {
+ Geom::OptRect bbox = item->documentVisualBounds();
+ if (bbox && bbox->intersects(*eraserBbox)) {
+ Inkscape::XML::Node* dup = this->repr->duplicate(xml_doc);
+ this->repr->parent()->appendChild(dup);
+ Inkscape::GC::release(dup); // parent takes over
+ Inkscape::ObjectSet w_selection(this->desktop);
+ w_selection.set(dup);
+ if (!this->nowidth) {
+ w_selection.pathUnion(true);
+ }
+ w_selection.add(item);
+ if(item->style->fill_rule.value == SP_WIND_RULE_EVENODD){
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule", "evenodd");
+ sp_desktop_set_style(this->desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+ if (this->nowidth) {
+ w_selection.pathCut(true);
+ } else {
+ w_selection.pathDiff(true);
+ }
+ workDone = true; // TODO set this only if something was cut.
+ bool break_apart = prefs->getBool("/tools/eraser/break_apart", false);
+ if(!break_apart){
+ w_selection.combine(true);
+ } else {
+ if(!this->nowidth){
+ w_selection.breakApart(true);
+ }
+ }
+ if ( !w_selection.isEmpty() ) {
+ // If the item was not completely erased, track the new remainder.
+ std::vector<SPItem*> nowSel(w_selection.items().begin(), w_selection.items().end());
+ for (auto i2 : nowSel) {
+ remainingItems.push_back(i2);
+ }
+ }
+ } else {
+ remainingItems.push_back(item);
+ }
+ }
+ }
+ } else if (eraser_mode == ERASER_MODE_CLIP) {
+ if (!this->nowidth) {
+ remainingItems.clear();
+ for (auto item : toWorkOn){
+ Inkscape::ObjectSet w_selection(this->desktop);
+ Geom::OptRect bbox = item->documentVisualBounds();
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node* dup = this->repr->duplicate(xml_doc);
+ this->repr->parent()->appendChild(dup);
+ Inkscape::GC::release(dup); // parent takes over
+ w_selection.set(dup);
+ w_selection.pathUnion(true);
+ if (bbox && bbox->intersects(*eraserBbox)) {
+ SPClipPath *clip_path = item->getClipObject();
+ if (clip_path) {
+ std::vector<SPItem*> selected;
+ selected.push_back(SP_ITEM(clip_path->firstChild()));
+ std::vector<Inkscape::XML::Node*> to_select;
+ std::vector<SPItem*> items(selected);
+ sp_item_list_to_curves(items, selected, to_select);
+ Inkscape::XML::Node * clip_data = SP_ITEM(clip_path->firstChild())->getRepr();
+ if (!clip_data && !to_select.empty()) {
+ clip_data = *(to_select.begin());
+ }
+ if (clip_data) {
+ Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc);
+ if (dup_clip) {
+ SPItem * dup_clip_obj = SP_ITEM(item_repr->parent->appendChildRepr(dup_clip));
+ Inkscape::GC::release(dup_clip);
+ if (dup_clip_obj) {
+ dup_clip_obj->transform *=
+ item->getRelativeTransform(SP_ITEM(item_repr->parent));
+ dup_clip_obj->updateRepr();
+ clip_path->deleteObject(true);
+ w_selection.raiseToTop(true);
+ w_selection.add(dup_clip);
+ w_selection.pathDiff(true);
+ //SPItem * clip = SP_ITEM(*(w_selection.items().begin()));
+ }
+ }
+ }
+ } else {
+ Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
+ sp_desktop_apply_style_tool (this->desktop, rect_repr, "/tools/eraser", false);
+ SPRect * rect = SP_RECT(item_repr->parent->appendChildRepr(rect_repr));
+ Inkscape::GC::release(rect_repr);
+ rect->setPosition (bbox->left(), bbox->top(), bbox->width(), bbox->height());
+ rect->transform = SP_ITEM(rect->parent)->i2doc_affine().inverse();
+
+ rect->updateRepr();
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ w_selection.raiseToTop(true);
+ w_selection.add(rect);
+ w_selection.pathDiff(true);
+ }
+ w_selection.raiseToTop(true);
+ w_selection.add(item);
+ w_selection.setMask(true, false, true);
+ } else {
+ SPItem *erase_clip = w_selection.singleItem();
+ if (erase_clip) {
+ erase_clip->deleteObject(true);
+ }
+ }
+ workDone = true;
+ if (wasSelection) {
+ remainingItems.push_back(item);
+ }
+ }
+ }
+ } else {
+ for (auto item : toWorkOn) {
+ item->deleteObject(true);
+ workDone = true;
+ }
+ }
+
+ if (eraser_mode == ERASER_MODE_DELETE) {
+ selection->deleteItems();
+ remainingItems.clear();
+ }
+
+ selection->clear();
+ if ( wasSelection ) {
+ if ( !remainingItems.empty() ) {
+ selection->add(remainingItems.begin(), remainingItems.end());
+ }
+ }
+ }
+ // Remove the eraser stroke itself:
+ sp_repr_unparent( this->repr );
+ this->repr = nullptr;
+ }
+ } else {
+ if (this->repr) {
+ sp_repr_unparent(this->repr);
+ this->repr = nullptr;
+ }
+ }
+ if ( workDone ) {
+ DocumentUndo::done(document, SP_VERB_CONTEXT_ERASER, _("Draw eraser stroke"));
+ } else {
+ DocumentUndo::cancel(document);
+ }
+}
+
+static void
+add_cap(SPCurve *curve,
+ Geom::Point const &pre, Geom::Point const &from,
+ Geom::Point const &to, Geom::Point const &post,
+ double rounding)
+{
+ Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0);
+ double mag = Geom::L2(vel);
+
+ Geom::Point v_in = from - pre;
+ double mag_in = Geom::L2(v_in);
+
+ if ( mag_in > ERASER_EPSILON ) {
+ v_in = mag * v_in / mag_in;
+ } else {
+ v_in = Geom::Point(0, 0);
+ }
+
+ Geom::Point v_out = to - post;
+ double mag_out = Geom::L2(v_out);
+
+ if ( mag_out > ERASER_EPSILON ) {
+ v_out = mag * v_out / mag_out;
+ } else {
+ v_out = Geom::Point(0, 0);
+ }
+
+ if ( Geom::L2(v_in) > ERASER_EPSILON || Geom::L2(v_out) > ERASER_EPSILON ) {
+ curve->curveto(from + v_in, to + v_out, to);
+ }
+}
+
+void EraserTool::accumulate() {
+ // construct a crude outline of the eraser's path.
+ // this desperately needs to be rewritten to use the path outliner...
+ if ( !this->cal1->is_empty() && !this->cal2->is_empty() ) {
+ this->accumulated->reset(); /* Is this required ?? */
+ SPCurve *rev_cal2 = this->cal2->create_reverse();
+
+ g_assert(this->cal1->get_segment_count() > 0);
+ g_assert(rev_cal2->get_segment_count() > 0);
+ g_assert( ! this->cal1->first_path()->closed() );
+ g_assert( ! rev_cal2->first_path()->closed() );
+
+ Geom::BezierCurve const * dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>( this->cal1->first_segment() );
+ Geom::BezierCurve const * rev_cal2_firstseg = dynamic_cast<Geom::BezierCurve const *>( rev_cal2->first_segment() );
+ Geom::BezierCurve const * dc_cal1_lastseg = dynamic_cast<Geom::BezierCurve const *>( this->cal1->last_segment() );
+ Geom::BezierCurve const * rev_cal2_lastseg = dynamic_cast<Geom::BezierCurve const *>( rev_cal2->last_segment() );
+
+ g_assert( dc_cal1_firstseg );
+ g_assert( rev_cal2_firstseg );
+ g_assert( dc_cal1_lastseg );
+ g_assert( rev_cal2_lastseg );
+
+ this->accumulated->append(this->cal1, FALSE);
+ if(!this->nowidth) {
+ add_cap(this->accumulated,
+ dc_cal1_lastseg->finalPoint() - dc_cal1_lastseg->unitTangentAt(1),
+ dc_cal1_lastseg->finalPoint(),
+ rev_cal2_firstseg->initialPoint(),
+ rev_cal2_firstseg->initialPoint() + rev_cal2_firstseg->unitTangentAt(0),
+ this->cap_rounding);
+
+ this->accumulated->append(rev_cal2, TRUE);
+
+ add_cap(this->accumulated,
+ rev_cal2_lastseg->finalPoint() - rev_cal2_lastseg->unitTangentAt(1),
+ rev_cal2_lastseg->finalPoint(),
+ dc_cal1_firstseg->initialPoint(),
+ dc_cal1_firstseg->initialPoint() + dc_cal1_firstseg->unitTangentAt(0),
+ this->cap_rounding);
+
+ this->accumulated->closepath();
+ }
+
+ rev_cal2->unref();
+
+ this->cal1->reset();
+ this->cal2->reset();
+ }
+}
+
+static double square(double const x)
+{
+ return x * x;
+}
+
+void EraserTool::fit_and_split(bool release) {
+ double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_ERASER );
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->nowidth = prefs->getDouble( "/tools/eraser/width", 1) == 0;
+
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:R=%c]", release?'T':'F');
+#endif
+
+ if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE ))
+ return; // just clicked
+
+ if ( this->npoints == SAMPLING_SIZE - 1 || release ) {
+#define BEZIER_SIZE 4
+#define BEZIER_MAX_BEZIERS 8
+#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS )
+
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:#] this->npoints:%d, release:%s\n",
+ dc->npoints, release ? "TRUE" : "FALSE");
+#endif
+
+ /* Current eraser */
+ if ( this->cal1->is_empty() || this->cal2->is_empty() ) {
+ /* dc->npoints > 0 */
+ /* g_print("erasers(1|2) reset\n"); */
+ this->cal1->reset();
+ this->cal2->reset();
+
+ this->cal1->moveto(this->point1[0]);
+ this->cal2->moveto(this->point2[0]);
+ }
+
+ Geom::Point b1[BEZIER_MAX_LENGTH];
+ gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) );
+
+ Geom::Point b2[BEZIER_MAX_LENGTH];
+ gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) );
+
+ if ( nb1 != -1 && nb2 != -1 ) {
+ /* Fit and draw and reset state */
+#ifdef ERASER_VERBOSE
+ g_print("nb1:%d nb2:%d\n", nb1, nb2);
+#endif
+
+ /* CanvasShape */
+ if (! release) {
+ this->currentcurve->reset();
+ this->currentcurve->moveto(b1[0]);
+
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]);
+
+ for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) {
+ this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]);
+ }
+
+ // FIXME: this->segments is always NULL at this point??
+ if (this->segments.empty()) { // first segment
+ add_cap(this->currentcurve, b2[1], b2[0], b1[0], b1[1], this->cap_rounding);
+ }
+
+ this->currentcurve->closepath();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true);
+ }
+
+ /* Current eraser */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->cal1->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
+ this->cal2->curveto(bp2[1], bp2[2], bp2[3]);
+ }
+ } else {
+ /* fixme: ??? */
+#ifdef ERASER_VERBOSE
+ g_print("[fit_and_split] failed to fit-cubic.\n");
+#endif
+ this->draw_temporary_box();
+
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal1->lineto(this->point1[i]);
+ }
+
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal2->lineto(this->point2[i]);
+ }
+ }
+
+ /* Fit and draw and copy last point */
+#ifdef ERASER_VERBOSE
+ g_print("[%d]Yup\n", this->npoints);
+#endif
+ if (!release) {
+ gint eraser_mode = prefs->getInt("/tools/eraser/mode",2);
+ g_assert(!this->currentcurve->is_empty());
+
+ SPCanvasItem *cbp = sp_canvas_item_new(desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr);
+ SPCurve *curve = this->currentcurve->copy();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve, true);
+ curve->unref();
+
+ guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", true);
+ //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", false);
+ double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/eraser");
+ double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", true);
+ //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", false);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD);
+ //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing
+ //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ /* fixme: Cannot we cascade it to root more clearly? */
+ g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop);
+
+ this->segments.push_back(cbp);
+
+ if (eraser_mode == ERASER_MODE_DELETE) {
+ sp_canvas_item_hide(cbp);
+ sp_canvas_item_hide(this->currentshape);
+ }
+ }
+
+ this->point1[0] = this->point1[this->npoints - 1];
+ this->point2[0] = this->point2[this->npoints - 1];
+ this->npoints = 1;
+ } else {
+ this->draw_temporary_box();
+ }
+}
+
+void EraserTool::draw_temporary_box() {
+ this->currentcurve->reset();
+
+ this->currentcurve->moveto(this->point1[this->npoints-1]);
+
+ for (gint i = this->npoints-2; i >= 0; i--) {
+ this->currentcurve->lineto(this->point1[i]);
+ }
+
+ for (gint i = 0; i < this->npoints; i++) {
+ this->currentcurve->lineto(this->point2[i]);
+ }
+
+ if (this->npoints >= 2) {
+ add_cap(this->currentcurve, this->point2[this->npoints-2], this->point2[this->npoints-1], this->point1[this->npoints-1], this->point1[this->npoints-2], this->cap_rounding);
+ }
+
+ this->currentcurve->closepath();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, 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/tools/eraser-tool.h b/src/ui/tools/eraser-tool.h
new file mode 100644
index 0000000..6da1fa3
--- /dev/null
+++ b/src/ui/tools/eraser-tool.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SP_ERASER_CONTEXT_H_SEEN
+#define SP_ERASER_CONTEXT_H_SEEN
+
+/*
+ * Handwriting-like drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2008 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+
+#include "ui/tools/dynamic-base.h"
+
+#define ERC_MIN_PRESSURE 0.0
+#define ERC_MAX_PRESSURE 1.0
+#define ERC_DEFAULT_PRESSURE 1.0
+
+#define ERC_MIN_TILT -1.0
+#define ERC_MAX_TILT 1.0
+#define ERC_DEFAULT_TILT 0.0
+
+#define ERASER_MODE_DELETE 0
+#define ERASER_MODE_CUT 1
+#define ERASER_MODE_CLIP 2
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class EraserTool : public DynamicBase {
+public:
+ EraserTool();
+ ~EraserTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ void reset(Geom::Point p);
+ void extinput(GdkEvent *event);
+ bool apply(Geom::Point p);
+ void brush();
+ void cancel();
+ void clear_current();
+ void set_to_accumulated();
+ void accumulate();
+ void fit_and_split(bool release);
+ void draw_temporary_box();
+ bool nowidth;
+};
+
+}
+}
+}
+
+#endif // SP_ERASER_CONTEXT_H_SEEN
+
+/*
+ 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/tools/flood-tool.cpp b/src/ui/tools/flood-tool.cpp
new file mode 100644
index 0000000..0e72142
--- /dev/null
+++ b/src/ui/tools/flood-tool.cpp
@@ -0,0 +1,1254 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bucket fill drawing context, works by bitmap filling an area on a rendered version
+ * of the current display and then tracing the result using potrace.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * John Bintz <jcoswell@coswellproductions.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000-2005 authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "flood-tool.h"
+
+#include <cmath>
+#include <queue>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/pathvector.h>
+
+#include "color.h"
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection.h"
+#include "splivarot.h"
+#include "verbs.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing-context.h"
+#include "display/drawing-image.h"
+#include "display/drawing.h"
+#include "display/sp-canvas.h"
+
+#include "include/macros.h"
+
+#include "livarot/Path.h"
+#include "livarot/Shape.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+#include "object/sp-root.h"
+
+#include "ui/pixmaps/cursor-paintbucket.xpm"
+
+#include "svg/svg.h"
+
+#include "trace/imagemap.h"
+#include "trace/potrace/inkscape-potrace.h"
+
+#include "ui/shape-editor.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+using Inkscape::Display::ExtractARGB32;
+using Inkscape::Display::ExtractRGB32;
+using Inkscape::Display::AssembleARGB32;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& FloodTool::getPrefsPath() {
+ return FloodTool::prefsPath;
+}
+
+const std::string FloodTool::prefsPath = "/tools/paintbucket";
+
+// TODO: Replace by C++11 initialization
+// Must match PaintBucketChannels enum
+Glib::ustring ch_init[8] = {
+ _("Visible Colors"),
+ _("Red"),
+ _("Green"),
+ _("Blue"),
+ _("Hue"),
+ _("Saturation"),
+ _("Lightness"),
+ _("Alpha"),
+};
+const std::vector<Glib::ustring> FloodTool::channel_list( ch_init, ch_init+8 );
+
+Glib::ustring gap_init[4] = {
+ NC_("Flood autogap", "None"),
+ NC_("Flood autogap", "Small"),
+ NC_("Flood autogap", "Medium"),
+ NC_("Flood autogap", "Large")
+};
+const std::vector<Glib::ustring> FloodTool::gap_list( gap_init, gap_init+4 );
+
+FloodTool::FloodTool()
+ : ToolBase(cursor_paintbucket_xpm)
+ , item(nullptr)
+{
+ // TODO: Why does the flood tool use a hardcoded tolerance instead of a pref?
+ this->tolerance = 4;
+}
+
+FloodTool::~FloodTool() {
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->item) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ */
+void FloodTool::selection_changed(Inkscape::Selection* selection) {
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+}
+
+void FloodTool::setup() {
+ ToolBase::setup();
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = this->desktop->getSelection()->connectChanged(
+ sigc::mem_fun(this, &FloodTool::selection_changed)
+ );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/paintbucket/selcue")) {
+ this->enableSelectionCue();
+ }
+}
+
+
+// Changes from 0.48 -> 0.49 (Cairo)
+// 0.49: Ignores alpha in background
+// 0.48: RGBA, 0.49 ARGB
+// 0.49: premultiplied alpha
+inline static guint32 compose_onto(guint32 px, guint32 bg)
+{
+ guint ap = 0, rp = 0, gp = 0, bp = 0;
+ guint rb = 0, gb = 0, bb = 0;
+ ExtractARGB32(px, ap, rp, gp, bp);
+ ExtractRGB32(bg, rb, gb, bb);
+
+ // guint ao = 255*255 - (255-ap)*(255-bp); ao = (ao + 127) / 255;
+ // guint ao = (255-ap)*ab + 255*ap; ao = (ao + 127) / 255;
+ guint ao = 255; // Cairo version doesn't allow background to have alpha != 1.
+ guint ro = (255-ap)*rb + 255*rp; ro = (ro + 127) / 255;
+ guint go = (255-ap)*gb + 255*gp; go = (go + 127) / 255;
+ guint bo = (255-ap)*bb + 255*bp; bo = (bo + 127) / 255;
+
+ guint pxout = AssembleARGB32(ao, ro, go, bo);
+ return pxout;
+}
+
+/**
+ * Get the pointer to a pixel in a pixel buffer.
+ * @param px The pixel buffer.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ * @param stride The rowstride of the pixel buffer.
+ */
+inline guint32 get_pixel(guchar *px, int x, int y, int stride) {
+ return *reinterpret_cast<guint32*>(px + y * stride + x * 4);
+}
+
+inline unsigned char * get_trace_pixel(guchar *trace_px, int x, int y, int width) {
+ return trace_px + (x + y * width);
+}
+
+/**
+ * \brief Check whether two unsigned integers are close to each other
+ *
+ * \param[in] a The 1st unsigned int
+ * \param[in] b The 2nd unsigned int
+ * \param[in] d The threshold for comparison
+ *
+ * \return true if |a-b| <= d; false otherwise
+ */
+static bool compare_guint32(guint32 const a, guint32 const b, guint32 const d)
+{
+ const int difference = std::abs(static_cast<int>(a) - static_cast<int>(b));
+ return difference <= d;
+}
+
+/**
+ * Compare a pixel in a pixel buffer with another pixel to determine if a point should be included in the fill operation.
+ * @param check The pixel in the pixel buffer to check.
+ * @param orig The original selected pixel to use as the fill target color.
+ * @param merged_orig_pixel The original pixel merged with the background.
+ * @param dtc The desktop background color.
+ * @param threshold The fill threshold.
+ * @param method The fill method to use as defined in PaintBucketChannels.
+ */
+static bool compare_pixels(guint32 check, guint32 orig, guint32 merged_orig_pixel, guint32 dtc, int threshold, PaintBucketChannels method)
+{
+ float hsl_check[3] = {0,0,0}, hsl_orig[3] = {0,0,0};
+
+ guint32 ac = 0, rc = 0, gc = 0, bc = 0;
+ ExtractARGB32(check, ac, rc, gc, bc);
+
+ guint32 ao = 0, ro = 0, go = 0, bo = 0;
+ ExtractARGB32(orig, ao, ro, go, bo);
+
+ guint32 ad = 0, rd = 0, gd = 0, bd = 0;
+ ExtractARGB32(dtc, ad, rd, gd, bd);
+
+ guint32 amop = 0, rmop = 0, gmop = 0, bmop = 0;
+ ExtractARGB32(merged_orig_pixel, amop, rmop, gmop, bmop);
+
+ if ((method == FLOOD_CHANNELS_H) ||
+ (method == FLOOD_CHANNELS_S) ||
+ (method == FLOOD_CHANNELS_L)) {
+ double dac = ac;
+ double dao = ao;
+ SPColor::rgb_to_hsl_floatv(hsl_check, rc / dac, gc / dac, bc / dac);
+ SPColor::rgb_to_hsl_floatv(hsl_orig, ro / dao, go / dao, bo / dao);
+ }
+
+ switch (method) {
+ case FLOOD_CHANNELS_ALPHA:
+ return compare_guint32(ac, ao, threshold);
+ case FLOOD_CHANNELS_R:
+ return compare_guint32(ac ? unpremul_alpha(rc, ac) : 0,
+ ao ? unpremul_alpha(ro, ao) : 0,
+ threshold);
+ case FLOOD_CHANNELS_G:
+ return compare_guint32(ac ? unpremul_alpha(gc, ac) : 0,
+ ao ? unpremul_alpha(go, ao) : 0,
+ threshold);
+ case FLOOD_CHANNELS_B:
+ return compare_guint32(ac ? unpremul_alpha(bc, ac) : 0,
+ ao ? unpremul_alpha(bo, ao) : 0,
+ threshold);
+ case FLOOD_CHANNELS_RGB:
+ {
+ guint32 amc, rmc, bmc, gmc;
+ //amc = 255*255 - (255-ac)*(255-ad); amc = (amc + 127) / 255;
+ //amc = (255-ac)*ad + 255*ac; amc = (amc + 127) / 255;
+ amc = 255; // Why are we looking at desktop? Cairo version ignores destop alpha
+ rmc = (255-ac)*rd + 255*rc; rmc = (rmc + 127) / 255;
+ gmc = (255-ac)*gd + 255*gc; gmc = (gmc + 127) / 255;
+ bmc = (255-ac)*bd + 255*bc; bmc = (bmc + 127) / 255;
+
+ int diff = 0; // The total difference between each of the 3 color components
+ diff += std::abs(static_cast<int>(amc ? unpremul_alpha(rmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(rmop, amop) : 0));
+ diff += std::abs(static_cast<int>(amc ? unpremul_alpha(gmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(gmop, amop) : 0));
+ diff += std::abs(static_cast<int>(amc ? unpremul_alpha(bmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(bmop, amop) : 0));
+ return ((diff / 3) <= ((threshold * 3) / 4));
+ }
+ case FLOOD_CHANNELS_H:
+ return ((int)(fabs(hsl_check[0] - hsl_orig[0]) * 100.0) <= threshold);
+ case FLOOD_CHANNELS_S:
+ return ((int)(fabs(hsl_check[1] - hsl_orig[1]) * 100.0) <= threshold);
+ case FLOOD_CHANNELS_L:
+ return ((int)(fabs(hsl_check[2] - hsl_orig[2]) * 100.0) <= threshold);
+ }
+
+ return false;
+}
+
+enum {
+ PIXEL_CHECKED = 1,
+ PIXEL_QUEUED = 2,
+ PIXEL_PAINTABLE = 4,
+ PIXEL_NOT_PAINTABLE = 8,
+ PIXEL_COLORED = 16
+};
+
+static inline bool is_pixel_checked(unsigned char *t) { return (*t & PIXEL_CHECKED) == PIXEL_CHECKED; }
+static inline bool is_pixel_queued(unsigned char *t) { return (*t & PIXEL_QUEUED) == PIXEL_QUEUED; }
+static inline bool is_pixel_paintability_checked(unsigned char *t) {
+ return !((*t & PIXEL_PAINTABLE) == 0) && ((*t & PIXEL_NOT_PAINTABLE) == 0);
+}
+static inline bool is_pixel_paintable(unsigned char *t) { return (*t & PIXEL_PAINTABLE) == PIXEL_PAINTABLE; }
+static inline bool is_pixel_colored(unsigned char *t) { return (*t & PIXEL_COLORED) == PIXEL_COLORED; }
+
+static inline void mark_pixel_checked(unsigned char *t) { *t |= PIXEL_CHECKED; }
+static inline void mark_pixel_unchecked(unsigned char *t) { *t ^= PIXEL_CHECKED; }
+static inline void mark_pixel_queued(unsigned char *t) { *t |= PIXEL_QUEUED; }
+static inline void mark_pixel_paintable(unsigned char *t) { *t |= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; }
+static inline void mark_pixel_not_paintable(unsigned char *t) { *t |= PIXEL_NOT_PAINTABLE; *t ^= PIXEL_PAINTABLE; }
+static inline void mark_pixel_colored(unsigned char *t) { *t |= PIXEL_COLORED; }
+
+static inline void clear_pixel_paintability(unsigned char *t) { *t ^= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; }
+
+struct bitmap_coords_info {
+ bool is_left;
+ unsigned int x;
+ unsigned int y;
+ int y_limit;
+ unsigned int width;
+ unsigned int height;
+ unsigned int stride;
+ unsigned int threshold;
+ unsigned int radius;
+ PaintBucketChannels method;
+ guint32 dtc;
+ guint32 merged_orig_pixel;
+ Geom::Rect bbox;
+ Geom::Rect screen;
+ unsigned int max_queue_size;
+ unsigned int current_step;
+};
+
+/**
+ * Check if a pixel can be included in the fill.
+ * @param px The rendered pixel buffer to check.
+ * @param trace_t The pixel in the trace pixel buffer to check or mark.
+ * @param x The X coordinate.
+ * @param y The y coordinate.
+ * @param orig_color The original selected pixel to use as the fill target color.
+ * @param bci The bitmap_coords_info structure.
+ */
+inline static bool check_if_pixel_is_paintable(guchar *px, unsigned char *trace_t, int x, int y, guint32 orig_color, bitmap_coords_info bci) {
+ if (is_pixel_paintability_checked(trace_t)) {
+ return is_pixel_paintable(trace_t);
+ } else {
+ guint32 pixel = get_pixel(px, x, y, bci.stride);
+ if (compare_pixels(pixel, orig_color, bci.merged_orig_pixel, bci.dtc, bci.threshold, bci.method)) {
+ mark_pixel_paintable(trace_t);
+ return true;
+ } else {
+ mark_pixel_not_paintable(trace_t);
+ return false;
+ }
+ }
+}
+
+/**
+ * Perform the bitmap-to-vector tracing and place the traced path onto the document.
+ * @param px The trace pixel buffer to trace to SVG.
+ * @param desktop The desktop on which to place the final SVG path.
+ * @param transform The transform to apply to the final SVG path.
+ * @param union_with_selection If true, merge the final SVG path with the current selection.
+ */
+static void do_trace(bitmap_coords_info bci, guchar *trace_px, SPDesktop *desktop, Geom::Affine transform, unsigned int min_x, unsigned int max_x, unsigned int min_y, unsigned int max_y, bool union_with_selection) {
+ SPDocument *document = desktop->getDocument();
+
+ unsigned char *trace_t;
+
+ GrayMap *gray_map = GrayMapCreate((max_x - min_x + 1), (max_y - min_y + 1));
+ unsigned int gray_map_y = 0;
+ for (unsigned int y = min_y; y <= max_y; y++) {
+ unsigned long *gray_map_t = gray_map->rows[gray_map_y];
+
+ trace_t = get_trace_pixel(trace_px, min_x, y, bci.width);
+ for (unsigned int x = min_x; x <= max_x; x++) {
+ *gray_map_t = is_pixel_colored(trace_t) ? GRAYMAP_BLACK : GRAYMAP_WHITE;
+ gray_map_t++;
+ trace_t++;
+ }
+ gray_map_y++;
+ }
+
+ Inkscape::Trace::Potrace::PotraceTracingEngine pte;
+ pte.keepGoing = 1;
+ std::vector<Inkscape::Trace::TracingEngineResult> results = pte.traceGrayMap(gray_map);
+ gray_map->destroy(gray_map);
+
+ //XML Tree being used here directly while it shouldn't be...."
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+
+ long totalNodeCount = 0L;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0);
+
+ for (auto result : results) {
+ totalNodeCount += result.getNodeCount();
+
+ Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path");
+ /* Set style */
+ sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false);
+
+ Geom::PathVector pathv = sp_svg_read_pathv(result.getPathData().c_str());
+ Path *path = new Path;
+ path->LoadPathVector(pathv);
+
+ if (offset != 0) {
+
+ Shape *path_shape = new Shape();
+
+ path->ConvertWithBackData(0.03);
+ path->Fill(path_shape, 0);
+ delete path;
+
+ Shape *expanded_path_shape = new Shape();
+
+ expanded_path_shape->ConvertToShape(path_shape, fill_nonZero);
+ path_shape->MakeOffset(expanded_path_shape, offset * desktop->current_zoom(), join_round, 4);
+ expanded_path_shape->ConvertToShape(path_shape, fill_positive);
+
+ Path *expanded_path = new Path();
+
+ expanded_path->Reset();
+ expanded_path_shape->ConvertToForme(expanded_path);
+ expanded_path->ConvertEvenLines(1.0);
+ expanded_path->Simplify(1.0);
+
+ delete path_shape;
+ delete expanded_path_shape;
+
+ gchar *str = expanded_path->svg_dump_path();
+ if (str && *str) {
+ pathRepr->setAttribute("d", str);
+ g_free(str);
+ } else {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Too much inset</b>, the result is empty."));
+ Inkscape::GC::release(pathRepr);
+ g_free(str);
+ return;
+ }
+
+ delete expanded_path;
+
+ } else {
+ gchar *str = path->svg_dump_path();
+ delete path;
+ pathRepr->setAttribute("d", str);
+ g_free(str);
+ }
+
+ desktop->currentLayer()->addChild(pathRepr,nullptr);
+
+ SPObject *reprobj = document->getObjectByRepr(pathRepr);
+ if (reprobj) {
+ SP_ITEM(reprobj)->doWriteTransform(transform);
+
+ // premultiply the item transform by the accumulated parent transform in the paste layer
+ Geom::Affine local (SP_GROUP(desktop->currentLayer())->i2doc_affine());
+ if (!local.isIdentity()) {
+ gchar const *t_str = pathRepr->attribute("transform");
+ Geom::Affine item_t (Geom::identity());
+ if (t_str)
+ sp_svg_transform_read(t_str, &item_t);
+ item_t *= local.inverse();
+ // (we're dealing with unattached repr, so we write to its attr instead of using sp_item_set_transform)
+ gchar *affinestr=sp_svg_transform_write(item_t);
+ pathRepr->setAttribute("transform", affinestr);
+ g_free(affinestr);
+ }
+
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ pathRepr->setPosition(-1);
+
+ if (union_with_selection) {
+ desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE,
+ ngettext("Area filled, path with <b>%d</b> node created and unioned with selection.","Area filled, path with <b>%d</b> nodes created and unioned with selection.",
+ SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() );
+ selection->add(reprobj);
+ selection->pathUnion(true);
+ } else {
+ desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE,
+ ngettext("Area filled, path with <b>%d</b> node created.","Area filled, path with <b>%d</b> nodes created.",
+ SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() );
+ selection->set(reprobj);
+ }
+
+ }
+
+ Inkscape::GC::release(pathRepr);
+
+ }
+}
+
+/**
+ * The possible return states of perform_bitmap_scanline_check().
+ */
+enum ScanlineCheckResult {
+ SCANLINE_CHECK_OK,
+ SCANLINE_CHECK_ABORTED,
+ SCANLINE_CHECK_BOUNDARY
+};
+
+/**
+ * Determine if the provided coordinates are within the pixel buffer limits.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ * @param bci The bitmap_coords_info structure.
+ */
+inline static bool coords_in_range(unsigned int x, unsigned int y, bitmap_coords_info bci) {
+ return (x < bci.width) &&
+ (y < bci.height);
+}
+
+#define PAINT_DIRECTION_LEFT 1
+#define PAINT_DIRECTION_RIGHT 2
+#define PAINT_DIRECTION_UP 4
+#define PAINT_DIRECTION_DOWN 8
+#define PAINT_DIRECTION_ALL 15
+
+/**
+ * Paint a pixel or a square (if autogap is enabled) on the trace pixel buffer.
+ * @param px The rendered pixel buffer to check.
+ * @param trace_px The trace pixel buffer.
+ * @param orig_color The original selected pixel to use as the fill target color.
+ * @param bci The bitmap_coords_info structure.
+ * @param original_point_trace_t The original pixel in the trace pixel buffer to check.
+ */
+inline static unsigned int paint_pixel(guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned char *original_point_trace_t) {
+ if (bci.radius == 0) {
+ mark_pixel_colored(original_point_trace_t);
+ return PAINT_DIRECTION_ALL;
+ } else {
+ unsigned char *trace_t;
+
+ bool can_paint_up = true;
+ bool can_paint_down = true;
+ bool can_paint_left = true;
+ bool can_paint_right = true;
+
+ for (unsigned int ty = bci.y - bci.radius; ty <= bci.y + bci.radius; ty++) {
+ for (unsigned int tx = bci.x - bci.radius; tx <= bci.x + bci.radius; tx++) {
+ if (coords_in_range(tx, ty, bci)) {
+ trace_t = get_trace_pixel(trace_px, tx, ty, bci.width);
+ if (!is_pixel_colored(trace_t)) {
+ if (check_if_pixel_is_paintable(px, trace_t, tx, ty, orig_color, bci)) {
+ mark_pixel_colored(trace_t);
+ } else {
+ if (tx < bci.x) { can_paint_left = false; }
+ if (tx > bci.x) { can_paint_right = false; }
+ if (ty < bci.y) { can_paint_up = false; }
+ if (ty > bci.y) { can_paint_down = false; }
+ }
+ }
+ }
+ }
+ }
+
+ unsigned int paint_directions = 0;
+ if (can_paint_left) { paint_directions += PAINT_DIRECTION_LEFT; }
+ if (can_paint_right) { paint_directions += PAINT_DIRECTION_RIGHT; }
+ if (can_paint_up) { paint_directions += PAINT_DIRECTION_UP; }
+ if (can_paint_down) { paint_directions += PAINT_DIRECTION_DOWN; }
+
+ return paint_directions;
+ }
+}
+
+/**
+ * Push a point to be checked onto the bottom of the rendered pixel buffer check queue.
+ * @param fill_queue The fill queue to add the point to.
+ * @param max_queue_size The maximum size of the fill queue.
+ * @param trace_t The trace pixel buffer pixel.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ */
+static void push_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) {
+ if (!is_pixel_queued(trace_t)) {
+ if ((fill_queue->size() < max_queue_size)) {
+ fill_queue->push_back(Geom::Point(x, y));
+ mark_pixel_queued(trace_t);
+ }
+ }
+}
+
+/**
+ * Shift a point to be checked onto the top of the rendered pixel buffer check queue.
+ * @param fill_queue The fill queue to add the point to.
+ * @param max_queue_size The maximum size of the fill queue.
+ * @param trace_t The trace pixel buffer pixel.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ */
+static void shift_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) {
+ if (!is_pixel_queued(trace_t)) {
+ if ((fill_queue->size() < max_queue_size)) {
+ fill_queue->push_front(Geom::Point(x, y));
+ mark_pixel_queued(trace_t);
+ }
+ }
+}
+
+/**
+ * Scan a row in the rendered pixel buffer and add points to the fill queue as necessary.
+ * @param fill_queue The fill queue to add the point to.
+ * @param px The rendered pixel buffer.
+ * @param trace_px The trace pixel buffer.
+ * @param orig_color The original selected pixel to use as the fill target color.
+ * @param bci The bitmap_coords_info structure.
+ */
+static ScanlineCheckResult perform_bitmap_scanline_check(std::deque<Geom::Point> *fill_queue, guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned int *min_x, unsigned int *max_x) {
+ bool aborted = false;
+ bool reached_screen_boundary = false;
+ bool ok;
+
+ bool keep_tracing;
+ bool initial_paint = true;
+
+ unsigned char *current_trace_t = get_trace_pixel(trace_px, bci.x, bci.y, bci.width);
+ unsigned int paint_directions;
+
+ bool currently_painting_top = false;
+ bool currently_painting_bottom = false;
+
+ unsigned int top_ty = (bci.y > 0) ? bci.y - 1 : 0;
+ unsigned int bottom_ty = bci.y + 1;
+
+ bool can_paint_top = (top_ty > 0);
+ bool can_paint_bottom = (bottom_ty < bci.height);
+
+ Geom::Point front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front();
+
+ do {
+ ok = false;
+ if (bci.is_left) {
+ keep_tracing = (bci.x != 0);
+ } else {
+ keep_tracing = (bci.x < bci.width);
+ }
+
+ *min_x = MIN(*min_x, bci.x);
+ *max_x = MAX(*max_x, bci.x);
+
+ if (keep_tracing) {
+ if (check_if_pixel_is_paintable(px, current_trace_t, bci.x, bci.y, orig_color, bci)) {
+ paint_directions = paint_pixel(px, trace_px, orig_color, bci, current_trace_t);
+ if (bci.radius == 0) {
+ mark_pixel_checked(current_trace_t);
+ if ((!fill_queue->empty()) &&
+ (front_of_queue[Geom::X] == bci.x) &&
+ (front_of_queue[Geom::Y] == bci.y)) {
+ fill_queue->pop_front();
+ front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front();
+ }
+ }
+
+ if (can_paint_top) {
+ if (paint_directions & PAINT_DIRECTION_UP) {
+ unsigned char *trace_t = current_trace_t - bci.width;
+ if (!is_pixel_queued(trace_t)) {
+ bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, top_ty, orig_color, bci);
+
+ if (initial_paint) { currently_painting_top = !ok_to_paint; }
+
+ if (ok_to_paint && (!currently_painting_top)) {
+ currently_painting_top = true;
+ push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, top_ty);
+ }
+ if ((!ok_to_paint) && currently_painting_top) {
+ currently_painting_top = false;
+ }
+ }
+ }
+ }
+
+ if (can_paint_bottom) {
+ if (paint_directions & PAINT_DIRECTION_DOWN) {
+ unsigned char *trace_t = current_trace_t + bci.width;
+ if (!is_pixel_queued(trace_t)) {
+ bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, bottom_ty, orig_color, bci);
+
+ if (initial_paint) { currently_painting_bottom = !ok_to_paint; }
+
+ if (ok_to_paint && (!currently_painting_bottom)) {
+ currently_painting_bottom = true;
+ push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, bottom_ty);
+ }
+ if ((!ok_to_paint) && currently_painting_bottom) {
+ currently_painting_bottom = false;
+ }
+ }
+ }
+ }
+
+ if (bci.is_left) {
+ if (paint_directions & PAINT_DIRECTION_LEFT) {
+ bci.x--; current_trace_t--;
+ ok = true;
+ }
+ } else {
+ if (paint_directions & PAINT_DIRECTION_RIGHT) {
+ bci.x++; current_trace_t++;
+ ok = true;
+ }
+ }
+
+ initial_paint = false;
+ }
+ } else {
+ if (bci.bbox.min()[Geom::X] > bci.screen.min()[Geom::X]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+ } while (ok);
+
+ if (aborted) { return SCANLINE_CHECK_ABORTED; }
+ if (reached_screen_boundary) { return SCANLINE_CHECK_BOUNDARY; }
+ return SCANLINE_CHECK_OK;
+}
+
+/**
+ * Sort the rendered pixel buffer check queue vertically.
+ */
+static bool sort_fill_queue_vertical(Geom::Point a, Geom::Point b) {
+ return a[Geom::Y] > b[Geom::Y];
+}
+
+/**
+ * Sort the rendered pixel buffer check queue horizontally.
+ */
+static bool sort_fill_queue_horizontal(Geom::Point a, Geom::Point b) {
+ return a[Geom::X] > b[Geom::X];
+}
+
+/**
+ * Perform a flood fill operation.
+ * @param event_context The event context for this tool.
+ * @param event The details of this event.
+ * @param union_with_selection If true, union the new fill with the current selection.
+ * @param is_point_fill If false, use the Rubberband "touch selection" to get the initial points for the fill.
+ * @param is_touch_fill If true, use only the initial contact point in the Rubberband "touch selection" as the fill target color.
+ */
+static void sp_flood_do_flood_fill(ToolBase *event_context, GdkEvent *event, bool union_with_selection, bool is_point_fill, bool is_touch_fill) {
+ SPDesktop *desktop = event_context->desktop;
+ SPDocument *document = desktop->getDocument();
+
+ document->ensureUpToDate();
+
+ Geom::OptRect bbox = document->getRoot()->visualBounds();
+
+ if (!bbox) {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill."));
+ return;
+ }
+
+ // Render 160% of the physical display to the render pixel buffer, so that available
+ // fill areas off the screen can be included in the fill.
+ double padding = 1.6;
+
+ Geom::Rect screen = desktop->get_display_area();
+
+ // image space is world space with an offset
+ Geom::Rect const screen_world = screen * desktop->d2w();
+ Geom::IntPoint const img_dims = (screen_world.dimensions() * padding).ceil();
+ Geom::Affine const world2img = Geom::Translate((img_dims - screen_world.dimensions()) / 2.0 - screen_world.min());
+ Geom::Affine const doc2img = desktop->doc2dt() * desktop->d2w() * world2img;
+
+ auto const width = img_dims.x();
+ auto const height = img_dims.y();
+
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
+ guchar *px = g_new(guchar, stride * height);
+ guint32 bgcolor, dtc;
+
+ // Draw image into data block px
+ { // this block limits the lifetime of Drawing and DrawingContext
+ /* Create DrawingItems and set transform */
+ unsigned dkey = SPItem::display_key_new(1);
+ Inkscape::Drawing drawing;
+ Inkscape::DrawingItem *root = document->getRoot()->invoke_show( drawing, dkey, SP_ITEM_SHOW_DISPLAY);
+ root->setTransform(doc2img);
+ drawing.setRoot(root);
+
+ Geom::IntRect final_bbox = Geom::IntRect::from_xywh(0, 0, width, height);
+ drawing.update(final_bbox);
+
+ cairo_surface_t *s = cairo_image_surface_create_for_data(
+ px, CAIRO_FORMAT_ARGB32, width, height, stride);
+ Inkscape::DrawingContext dc(s, Geom::Point(0,0));
+ // cairo_translate not necessary here - surface origin is at 0,0
+
+ SPNamedView *nv = desktop->getNamedView();
+ bgcolor = nv->pagecolor;
+ // bgcolor is 0xrrggbbaa, we need 0xaarrggbb
+ dtc = (bgcolor >> 8) | (bgcolor << 24);
+
+ dc.setSource(bgcolor);
+ dc.setOperator(CAIRO_OPERATOR_SOURCE);
+ dc.paint();
+ dc.setOperator(CAIRO_OPERATOR_OVER);
+
+ drawing.render(dc, final_bbox);
+
+ //cairo_surface_write_to_png( s, "cairo.png" );
+
+ cairo_surface_flush(s);
+ cairo_surface_destroy(s);
+
+ // Hide items
+ document->getRoot()->invoke_hide(dkey);
+ }
+
+ // {
+ // // Dump data to png
+ // cairo_surface_t *s = cairo_image_surface_create_for_data(
+ // px, CAIRO_FORMAT_ARGB32, width, height, stride);
+ // cairo_surface_write_to_png( s, "cairo2.png" );
+ // std::cout << " Wrote cairo2.png" << std::endl;
+ // }
+
+ guchar *trace_px = g_new(guchar, width * height);
+ memset(trace_px, 0x00, width * height);
+
+ std::deque<Geom::Point> fill_queue;
+ std::queue<Geom::Point> color_queue;
+
+ std::vector<Geom::Point> fill_points;
+
+ bool aborted = false;
+ int y_limit = height - 1;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ PaintBucketChannels method = (PaintBucketChannels) prefs->getInt("/tools/paintbucket/channels", 0);
+ int threshold = prefs->getIntLimited("/tools/paintbucket/threshold", 1, 0, 100);
+
+ switch(method) {
+ case FLOOD_CHANNELS_ALPHA:
+ case FLOOD_CHANNELS_RGB:
+ case FLOOD_CHANNELS_R:
+ case FLOOD_CHANNELS_G:
+ case FLOOD_CHANNELS_B:
+ threshold = (255 * threshold) / 100;
+ break;
+ case FLOOD_CHANNELS_H:
+ case FLOOD_CHANNELS_S:
+ case FLOOD_CHANNELS_L:
+ break;
+ }
+
+ bitmap_coords_info bci;
+
+ bci.y_limit = y_limit;
+ bci.width = width;
+ bci.height = height;
+ bci.stride = stride;
+ bci.threshold = threshold;
+ bci.method = method;
+ bci.bbox = *bbox;
+ bci.screen = screen;
+ bci.dtc = dtc;
+ bci.radius = prefs->getIntLimited("/tools/paintbucket/autogap", 0, 0, 3);
+ bci.max_queue_size = (width * height) / 4;
+ bci.current_step = 0;
+
+ if (is_point_fill) {
+ fill_points.emplace_back(event->button.x, event->button.y);
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+ fill_points = r->getPoints();
+ }
+
+ auto const img_max_indices = Geom::Rect::from_xywh(0, 0, width - 1, height - 1);
+
+ for (unsigned int i = 0; i < fill_points.size(); i++) {
+ Geom::Point pw = fill_points[i] * world2img;
+
+ pw = img_max_indices.clamp(pw);
+
+ if (is_touch_fill) {
+ if (i == 0) {
+ color_queue.push(pw);
+ } else {
+ unsigned char *trace_t = get_trace_pixel(trace_px, (int)pw[Geom::X], (int)pw[Geom::Y], width);
+ push_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, (int)pw[Geom::X], (int)pw[Geom::Y]);
+ }
+ } else {
+ color_queue.push(pw);
+ }
+ }
+
+ bool reached_screen_boundary = false;
+
+ bool first_run = true;
+
+ unsigned long sort_size_threshold = 5;
+
+ unsigned int min_y = height;
+ unsigned int max_y = 0;
+ unsigned int min_x = width;
+ unsigned int max_x = 0;
+
+ while (!color_queue.empty() && !aborted) {
+ Geom::Point color_point = color_queue.front();
+ color_queue.pop();
+
+ int cx = (int)color_point[Geom::X];
+ int cy = (int)color_point[Geom::Y];
+
+ guint32 orig_color = get_pixel(px, cx, cy, stride);
+ bci.merged_orig_pixel = compose_onto(orig_color, dtc);
+
+ unsigned char *trace_t = get_trace_pixel(trace_px, cx, cy, width);
+ if (!is_pixel_checked(trace_t) && !is_pixel_colored(trace_t)) {
+ if (check_if_pixel_is_paintable(px, trace_px, cx, cy, orig_color, bci)) {
+ shift_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, cx, cy);
+
+ if (!first_run) {
+ for (unsigned int y = 0; y < height; y++) {
+ trace_t = get_trace_pixel(trace_px, 0, y, width);
+ for (unsigned int x = 0; x < width; x++) {
+ clear_pixel_paintability(trace_t);
+ trace_t++;
+ }
+ }
+ }
+ first_run = false;
+ }
+ }
+
+ unsigned long old_fill_queue_size = fill_queue.size();
+
+ while (!fill_queue.empty() && !aborted) {
+ Geom::Point cp = fill_queue.front();
+
+ if (bci.radius == 0) {
+ unsigned long new_fill_queue_size = fill_queue.size();
+
+ /*
+ * To reduce the number of points in the fill queue, periodically
+ * resort all of the points in the queue so that scanline checks
+ * can complete more quickly. A point cannot be checked twice
+ * in a normal scanline checks, so forcing scanline checks to start
+ * from one corner of the rendered area as often as possible
+ * will reduce the number of points that need to be checked and queued.
+ */
+ if (new_fill_queue_size > sort_size_threshold) {
+ if (new_fill_queue_size > old_fill_queue_size) {
+ std::sort(fill_queue.begin(), fill_queue.end(), sort_fill_queue_vertical);
+
+ std::deque<Geom::Point>::iterator start_sort = fill_queue.begin();
+ std::deque<Geom::Point>::iterator end_sort = fill_queue.begin();
+ unsigned int sort_y = (unsigned int)cp[Geom::Y];
+ unsigned int current_y;
+
+ for (std::deque<Geom::Point>::iterator i = fill_queue.begin(); i != fill_queue.end(); ++i) {
+ Geom::Point current = *i;
+ current_y = (unsigned int)current[Geom::Y];
+ if (current_y != sort_y) {
+ if (start_sort != end_sort) {
+ std::sort(start_sort, end_sort, sort_fill_queue_horizontal);
+ }
+ sort_y = current_y;
+ start_sort = i;
+ }
+ end_sort = i;
+ }
+ if (start_sort != end_sort) {
+ std::sort(start_sort, end_sort, sort_fill_queue_horizontal);
+ }
+
+ cp = fill_queue.front();
+ }
+ }
+
+ old_fill_queue_size = new_fill_queue_size;
+ }
+
+ fill_queue.pop_front();
+
+ int x = (int)cp[Geom::X];
+ int y = (int)cp[Geom::Y];
+
+ min_y = MIN((unsigned int)y, min_y);
+ max_y = MAX((unsigned int)y, max_y);
+
+ unsigned char *trace_t = get_trace_pixel(trace_px, x, y, width);
+ if (!is_pixel_checked(trace_t)) {
+ mark_pixel_checked(trace_t);
+
+ if (y == 0) {
+ if (bbox->min()[Geom::Y] > screen.min()[Geom::Y]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+
+ if (y == y_limit) {
+ if (bbox->max()[Geom::Y] < screen.max()[Geom::Y]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+
+ bci.is_left = true;
+ bci.x = x;
+ bci.y = y;
+
+ ScanlineCheckResult result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x);
+
+ switch (result) {
+ case SCANLINE_CHECK_ABORTED:
+ aborted = true;
+ break;
+ case SCANLINE_CHECK_BOUNDARY:
+ reached_screen_boundary = true;
+ break;
+ default:
+ break;
+ }
+
+ if (bci.x < width) {
+ trace_t++;
+ if (!is_pixel_checked(trace_t) && !is_pixel_queued(trace_t)) {
+ mark_pixel_checked(trace_t);
+ bci.is_left = false;
+ bci.x = x + 1;
+
+ result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x);
+
+ switch (result) {
+ case SCANLINE_CHECK_ABORTED:
+ aborted = true;
+ break;
+ case SCANLINE_CHECK_BOUNDARY:
+ reached_screen_boundary = true;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ bci.current_step++;
+
+ if (bci.current_step > bci.max_queue_size) {
+ aborted = true;
+ }
+ }
+ }
+
+ g_free(px);
+
+ if (aborted) {
+ g_free(trace_px);
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill."));
+ return;
+ }
+
+ if (reached_screen_boundary) {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Only the visible part of the bounded area was filled.</b> If you want to fill all of the area, undo, zoom out, and fill again."));
+ }
+
+ unsigned int trace_padding = bci.radius + 1;
+ if (min_y > trace_padding) { min_y -= trace_padding; }
+ if (max_y < (y_limit - trace_padding)) { max_y += trace_padding; }
+ if (min_x > trace_padding) { min_x -= trace_padding; }
+ if (max_x < (width - 1 - trace_padding)) { max_x += trace_padding; }
+
+ Geom::Affine inverted_affine = Geom::Translate(min_x, min_y) * doc2img.inverse();
+
+ do_trace(bci, trace_px, desktop, inverted_affine, min_x, max_x, min_y, max_y, union_with_selection);
+
+ g_free(trace_px);
+
+ DocumentUndo::done(document, SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area"));
+}
+
+bool FloodTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if ((event->button.state & GDK_CONTROL_MASK) && event->button.button == 1 && !this->space_panning) {
+ Geom::Point const button_w(event->button.x, event->button.y);
+
+ SPItem *item = sp_event_context_find_item (desktop, button_w, TRUE, TRUE);
+
+ // Set style
+ desktop->applyCurrentOrToolStyle(item, "/tools/paintbucket", false);
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_PAINTBUCKET, _("Set style on object"));
+ // Dead assignment: Value stored to 'ret' is never read
+ //ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+// if (((ToolBaseClass *) sp_flood_context_parent_class)->item_handler) {
+// ret = ((ToolBaseClass *) sp_flood_context_parent_class)->item_handler(event_context, item, event);
+// }
+ // CPPIFY: ret is overwritten...
+ ret = ToolBase::item_handler(item, event);
+
+ return ret;
+}
+
+bool FloodTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ if (!(event->button.state & GDK_CONTROL_MASK)) {
+ Geom::Point const button_w(event->button.x, event->button.y);
+
+ if (Inkscape::have_viable_layer(desktop, this->defaultMessageContext())) {
+ // save drag origin
+ this->xp = (gint) button_w[Geom::X];
+ this->yp = (gint) button_w[Geom::Y];
+ this->within_tolerance = true;
+
+ dragging = true;
+
+ Geom::Point const p(desktop->w2d(button_w));
+ Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ Inkscape::Rubberband::get(desktop)->start(desktop, p);
+ }
+ }
+ }
+
+ case GDK_MOTION_NOTIFY:
+ if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+
+ this->within_tolerance = false;
+
+ Geom::Point const motion_pt(event->motion.x, event->motion.y);
+ Geom::Point const p(desktop->w2d(motion_pt));
+
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->move(p);
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> areas to add to fill, hold <b>Alt</b> for touch fill"));
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && !this->space_panning) {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+
+ if (r->is_started()) {
+ // set "busy" cursor
+ desktop->setWaitingCursor();
+
+ if (SP_IS_EVENT_CONTEXT(this)) {
+ // Since setWaitingCursor runs main loop iterations, we may have already left this tool!
+ // So check if the tool is valid before doing anything
+ dragging = false;
+
+ bool is_point_fill = this->within_tolerance;
+ bool is_touch_fill = event->button.state & GDK_MOD1_MASK;
+
+ sp_flood_do_flood_fill(this, event, event->button.state & GDK_SHIFT_MASK, is_point_fill, is_touch_fill);
+
+ desktop->clearWaitingCursor();
+ // restore cursor when done; note that it may already be different if e.g. user
+ // switched to another tool during interruptible tracing or drawing, in which case do nothing
+
+ ret = TRUE;
+ }
+
+ r->stop();
+
+ //if (SP_IS_EVENT_CONTEXT(this)) {
+ this->defaultMessageContext()->clear();
+ //}
+ }
+ }
+ break;
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // prevent the zoom field from activation
+ if (!MOD__CTRL_ONLY(event))
+ ret = TRUE;
+ break;
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void FloodTool::finishItem() {
+ this->message_context->clear();
+
+ if (this->item != nullptr) {
+ this->item->updateRepr();
+
+ desktop->canvas->endForcedFullRedraws();
+
+ desktop->getSelection()->set(this->item);
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area"));
+
+ this->item = nullptr;
+ }
+}
+
+void FloodTool::set_channels(gint channels) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/tools/paintbucket/channels", channels);
+}
+
+}
+}
+}
+
+/*
+ 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/tools/flood-tool.h b/src/ui/tools/flood-tool.h
new file mode 100644
index 0000000..877db98
--- /dev/null
+++ b/src/ui/tools/flood-tool.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_FLOOD_CONTEXT_H__
+#define __SP_FLOOD_CONTEXT_H__
+
+/*
+ * Flood fill drawing context
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * John Bintz <jcoswell@coswellproductions.org>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <vector>
+
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+
+#define SP_FLOOD_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FloodTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_FLOOD_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FloodTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class FloodTool : public ToolBase {
+public:
+ FloodTool();
+ ~FloodTool() override;
+
+ SPItem *item;
+
+ sigc::connection sel_changed_connection;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+ static void set_channels(gint channels);
+ static const std::vector<Glib::ustring> channel_list;
+ static const std::vector<Glib::ustring> gap_list;
+
+private:
+ void selection_changed(Inkscape::Selection* selection);
+ void finishItem();
+};
+
+enum PaintBucketChannels {
+ FLOOD_CHANNELS_RGB,
+ FLOOD_CHANNELS_R,
+ FLOOD_CHANNELS_G,
+ FLOOD_CHANNELS_B,
+ FLOOD_CHANNELS_H,
+ FLOOD_CHANNELS_S,
+ FLOOD_CHANNELS_L,
+ FLOOD_CHANNELS_ALPHA
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/freehand-base.cpp b/src/ui/tools/freehand-base.cpp
new file mode 100644
index 0000000..34faf81
--- /dev/null
+++ b/src/ui/tools/freehand-base.cpp
@@ -0,0 +1,1112 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Generic drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2012 Johan Engelen
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define DRAW_VERBOSE
+
+#include "desktop-style.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+
+#include "include/macros.h"
+
+#include "live_effects/lpe-bendpath.h"
+#include "live_effects/lpe-patternalongpath.h"
+#include "live_effects/lpe-simplify.h"
+#include "live_effects/lpe-powerstroke.h"
+
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+
+#include "id-clash.h"
+#include "object/sp-item-group.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-use.h"
+#include "style.h"
+
+#include "ui/clipboard.h"
+#include "ui/control-manager.h"
+#include "ui/draw-anchor.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/tools/pen-tool.h"
+#include "ui/tools/pencil-tool.h"
+
+#define MIN_PRESSURE 0.0
+#define MAX_PRESSURE 1.0
+#define DEFAULT_PRESSURE 1.0
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc);
+static void spdc_selection_modified(Inkscape::Selection *sel, guint flags, FreehandBase *dc);
+
+static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection *sel);
+
+/**
+ * Flushes white curve(s) and additional curve into object.
+ *
+ * No cleaning of colored curves - this has to be done by caller
+ * No rereading of white data, so if you cannot rely on ::modified, do it in caller
+ */
+static void spdc_flush_white(FreehandBase *dc, SPCurve *gc);
+
+static void spdc_reset_white(FreehandBase *dc);
+static void spdc_free_colors(FreehandBase *dc);
+
+FreehandBase::FreehandBase(gchar const *const *cursor_shape)
+ : ToolBase(cursor_shape)
+ , selection(nullptr)
+ , grab(nullptr)
+ , attach(false)
+ , red_color(0xff00007f)
+ , blue_color(0x0000ff7f)
+ , green_color(0x00ff007f)
+ , highlight_color(0x0000007f)
+ , red_bpath(nullptr)
+ , red_curve(nullptr)
+ , blue_bpath(nullptr)
+ , blue_curve(nullptr)
+ , green_curve(nullptr)
+ , green_anchor(nullptr)
+ , green_closed(false)
+ , white_item(nullptr)
+ , sa_overwrited(nullptr)
+ , sa(nullptr)
+ , ea(nullptr)
+ , waiting_LPE_type(Inkscape::LivePathEffect::INVALID_LPE)
+ , red_curve_is_valid(false)
+ , anchor_statusbar(false)
+ , tablet_enabled(false)
+ , is_tablet(false)
+ , pressure(DEFAULT_PRESSURE)
+{
+}
+
+FreehandBase::~FreehandBase() {
+ if (this->grab) {
+ sp_canvas_item_ungrab(this->grab);
+ this->grab = nullptr;
+ }
+
+ if (this->selection) {
+ this->selection = nullptr;
+ }
+
+ spdc_free_colors(this);
+}
+
+void FreehandBase::setup() {
+ ToolBase::setup();
+
+ this->selection = desktop->getSelection();
+
+ // Connect signals to track selection changes
+ this->sel_changed_connection = this->selection->connectChanged(
+ sigc::bind(sigc::ptr_fun(&spdc_selection_changed), this)
+ );
+ this->sel_modified_connection = this->selection->connectModified(
+ sigc::bind(sigc::ptr_fun(&spdc_selection_modified), this)
+ );
+
+ // Create red bpath
+ this->red_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ // Create red curve
+ this->red_curve = new SPCurve();
+
+ // Create blue bpath
+ this->blue_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->blue_bpath), this->blue_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ // Create blue curve
+ this->blue_curve = new SPCurve();
+
+ // Create green curve
+ this->green_curve = new SPCurve();
+
+ // No green anchor by default
+ this->green_anchor = nullptr;
+ this->green_closed = FALSE;
+
+ // Create start anchor alternative curve
+ this->sa_overwrited = new SPCurve();
+
+ this->attach = TRUE;
+ spdc_attach_selection(this, this->selection);
+}
+
+void FreehandBase::finish() {
+ this->sel_changed_connection.disconnect();
+ this->sel_modified_connection.disconnect();
+
+ if (this->grab) {
+ sp_canvas_item_ungrab(this->grab);
+ }
+
+ if (this->selection) {
+ this->selection = nullptr;
+ }
+
+ spdc_free_colors(this);
+
+ ToolBase::finish();
+}
+
+void FreehandBase::set(const Inkscape::Preferences::Entry& /*value*/) {
+}
+
+bool FreehandBase::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // prevent the zoom field from activation
+ if (!MOD__CTRL_ONLY(event)) {
+ ret = TRUE;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+static Glib::ustring const tool_name(FreehandBase *dc)
+{
+ return ( SP_IS_PEN_CONTEXT(dc)
+ ? "/tools/freehand/pen"
+ : "/tools/freehand/pencil" );
+}
+
+static void spdc_paste_curve_as_freehand_shape(Geom::PathVector const &newpath, FreehandBase *dc, SPItem *item)
+{
+ using namespace Inkscape::LivePathEffect;
+
+ // TODO: Don't paste path if nothing is on the clipboard
+ SPDocument *document = dc->desktop->doc();
+ bool saved = DocumentUndo::getUndoSensitive(document);
+ DocumentUndo::setUndoSensitive(document, false);
+ Effect::createAndApply(PATTERN_ALONG_PATH, dc->desktop->doc(), item);
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ static_cast<LPEPatternAlongPath*>(lpe)->pattern.set_new_value(newpath,true);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/live_effects/pap/width", 1);
+ if (!scale) {
+ scale = 1 / document->getDocumentScale()[0];
+ }
+ Inkscape::SVGOStringStream os;
+ os << scale;
+ lpe->getRepr()->setAttribute("prop_scale", os.str());
+ DocumentUndo::setUndoSensitive(document, saved);
+}
+
+void spdc_apply_style(SPObject *obj)
+{
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (obj->style) {
+ if (obj->style->stroke.isPaintserver()) {
+ SPPaintServer *server = obj->style->getStrokePaintServer();
+ if (server) {
+ Glib::ustring str;
+ str += "url(#";
+ str += server->getId();
+ str += ")";
+ sp_repr_css_set_property(css, "fill", str.c_str());
+ }
+ } else if (obj->style->stroke.isColor()) {
+ gchar c[64];
+ sp_svg_write_color(
+ c, sizeof(c),
+ obj->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value)));
+ sp_repr_css_set_property(css, "fill", c);
+ } else {
+ sp_repr_css_set_property(css, "fill", "none");
+ }
+ } else {
+ sp_repr_css_unset_property(css, "fill");
+ }
+
+ sp_repr_css_set_property(css, "fill-rule", "nonzero");
+ sp_repr_css_set_property(css, "stroke", "none");
+
+ sp_desktop_apply_css_recursive(obj, css, true);
+ sp_repr_css_attr_unref(css);
+}
+static void spdc_apply_powerstroke_shape(std::vector<Geom::Point> points, FreehandBase *dc, SPItem *item,
+ gint maxrecursion = 0)
+{
+ using namespace Inkscape::LivePathEffect;
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!document || !desktop) {
+ return;
+ }
+ if (SP_IS_PENCIL_CONTEXT(dc)) {
+ PencilTool *pt = SP_PENCIL_CONTEXT(dc);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (dc->tablet_enabled) {
+ SPObject *elemref = nullptr;
+ if ((elemref = document->getObjectById("power_stroke_preview"))) {
+ elemref->getRepr()->removeAttribute("style");
+ SPItem *successor = dynamic_cast<SPItem *>(elemref);
+ sp_desktop_apply_style_tool(desktop, successor->getRepr(),
+ Glib::ustring("/tools/freehand/pencil").data(), false);
+ spdc_apply_style(successor);
+ item->deleteObject(true);
+ item = successor;
+ dc->selection->set(item);
+ item->setLocked(false);
+ dc->white_item = item;
+ rename_id(SP_OBJECT(item), "path-1");
+ }
+ return;
+ }
+ }
+ bool saved = DocumentUndo::getUndoSensitive(document);
+ DocumentUndo::setUndoSensitive(document, false);
+ Effect::createAndApply(POWERSTROKE, dc->desktop->doc(), item);
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+
+ static_cast<LPEPowerStroke*>(lpe)->offset_points.param_set_and_write_new_value(points);
+
+ // write powerstroke parameters:
+ lpe->getRepr()->setAttribute("start_linecap_type", "zerowidth");
+ lpe->getRepr()->setAttribute("end_linecap_type", "zerowidth");
+ lpe->getRepr()->setAttribute("sort_points", "true");
+ lpe->getRepr()->setAttribute("interpolator_type", "CubicBezierJohan");
+ lpe->getRepr()->setAttribute("interpolator_beta", "0.2");
+ lpe->getRepr()->setAttribute("miter_limit", "4");
+ lpe->getRepr()->setAttribute("scale_width", "1");
+ lpe->getRepr()->setAttribute("linejoin_type", "extrp_arc");
+ DocumentUndo::setUndoSensitive(document, saved);
+}
+
+static void spdc_apply_bend_shape(gchar const *svgd, FreehandBase *dc, SPItem *item)
+{
+ using namespace Inkscape::LivePathEffect;
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if ( use ) {
+ return;
+ }
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!document || !desktop) {
+ return;
+ }
+ bool saved = DocumentUndo::getUndoSensitive(document);
+ DocumentUndo::setUndoSensitive(document, false);
+ if(!SP_IS_LPE_ITEM(item) || !SP_LPE_ITEM(item)->hasPathEffectOfType(BEND_PATH)){
+ Effect::createAndApply(BEND_PATH, dc->desktop->doc(), item);
+ }
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+
+ // write bend parameters:
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/live_effects/bend/width", 1);
+ if (!scale) {
+ scale = 1;
+ }
+ Inkscape::SVGOStringStream os;
+ os << scale;
+ lpe->getRepr()->setAttribute("prop_scale", os.str());
+ lpe->getRepr()->setAttribute("scale_y_rel", "false");
+ lpe->getRepr()->setAttribute("vertical", "false");
+ static_cast<LPEBendPath*>(lpe)->bend_path.paste_param_path(svgd);
+ DocumentUndo::setUndoSensitive(document, saved);
+}
+
+static void spdc_apply_simplify(std::string threshold, FreehandBase *dc, SPItem *item)
+{
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!document || !desktop) {
+ return;
+ }
+ bool saved = DocumentUndo::getUndoSensitive(document);
+ DocumentUndo::setUndoSensitive(document, false);
+ using namespace Inkscape::LivePathEffect;
+
+ Effect::createAndApply(SIMPLIFY, document, item);
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ // write simplify parameters:
+ lpe->getRepr()->setAttribute("steps", "1");
+ lpe->getRepr()->setAttributeOrRemoveIfEmpty("threshold", threshold);
+ lpe->getRepr()->setAttribute("smooth_angles", "360");
+ lpe->getRepr()->setAttribute("helper_size", "0");
+ lpe->getRepr()->setAttribute("simplify_individual_paths", "false");
+ lpe->getRepr()->setAttribute("simplify_just_coalesce", "false");
+ DocumentUndo::setUndoSensitive(document, saved);
+}
+
+enum shapeType { NONE, TRIANGLE_IN, TRIANGLE_OUT, ELLIPSE, CLIPBOARD, BEND_CLIPBOARD, LAST_APPLIED };
+static shapeType previous_shape_type = NONE;
+
+static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve *curve, bool is_bend)
+{
+ using namespace Inkscape::LivePathEffect;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (item && SP_IS_LPE_ITEM(item)) {
+ //Store the clipboard path to apply in the future without the use of clipboard
+ static Geom::PathVector previous_shape_pathv;
+ static SPItem *bend_item;
+ shapeType shape = (shapeType)prefs->getInt(tool_name(dc) + "/shape", 0);
+ if (previous_shape_type == NONE) {
+ previous_shape_type = shape;
+ }
+ if(shape == LAST_APPLIED){
+ shape = previous_shape_type;
+ if(shape == CLIPBOARD || shape == BEND_CLIPBOARD){
+ shape = LAST_APPLIED;
+ }
+ }
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ if (is_bend &&
+ (shape == BEND_CLIPBOARD || (shape == LAST_APPLIED && previous_shape_type != CLIPBOARD)) &&
+ cm->paste(SP_ACTIVE_DESKTOP,true))
+ {
+ bend_item = dc->selection->singleItem();
+ if(!bend_item || (!SP_IS_SHAPE(bend_item) && !SP_IS_GROUP(bend_item))){
+ previous_shape_type = NONE;
+ return;
+ }
+ } else if(is_bend) {
+ return;
+ }
+ if (!is_bend && previous_shape_type == BEND_CLIPBOARD && shape == BEND_CLIPBOARD) {
+ return;
+ }
+ bool shape_applied = false;
+ bool simplify = prefs->getInt(tool_name(dc) + "/simplify", 0);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ if(simplify && mode != 2){
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0);
+ tol = tol/(100.0*(102.0-tol));
+ std::ostringstream ss;
+ ss << tol;
+ spdc_apply_simplify(ss.str(), dc, item);
+ sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false);
+ }
+ if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1) {
+ Effect::createAndApply(SPIRO, dc->desktop->doc(), item);
+ }
+
+ if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2) {
+ Effect::createAndApply(BSPLINE, dc->desktop->doc(), item);
+ }
+ SPShape *sp_shape = dynamic_cast<SPShape *>(item);
+ if (sp_shape) {
+ curve = sp_shape->getCurve();
+ }
+ SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS);
+ const char *cstroke = sp_repr_css_property(css_item, "stroke", "none");
+ const char *cfill = sp_repr_css_property(css_item, "fill", "none");
+ const char *stroke_width = sp_repr_css_property(css_item, "stroke-width", "0");
+ double swidth;
+ sp_svg_number_read_d(stroke_width, &swidth);
+ swidth = prefs->getDouble("/live_effects/powerstroke/width", swidth/2);
+ if (!swidth) {
+ swidth = swidth/2;
+ }
+ swidth = std::abs(swidth);
+ if (SP_IS_PENCIL_CONTEXT(dc)) {
+ if (dc->tablet_enabled) {
+ std::vector<Geom::Point> points;
+ spdc_apply_powerstroke_shape(points, dc, item);
+ shape_applied = true;
+ shape = NONE;
+ previous_shape_type = NONE;
+ }
+ }
+
+#define SHAPE_LENGTH 10
+#define SHAPE_HEIGHT 10
+
+ switch (shape) {
+ case NONE:
+ // don't apply any shape
+ break;
+ case TRIANGLE_IN:
+ {
+ // "triangle in"
+ std::vector<Geom::Point> points(1);
+
+ points[0] = Geom::Point(0., swidth);
+ //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse();
+ spdc_apply_powerstroke_shape(points, dc, item);
+
+ shape_applied = true;
+ break;
+ }
+ case TRIANGLE_OUT:
+ {
+ // "triangle out"
+ guint curve_length = curve->get_segment_count();
+ std::vector<Geom::Point> points(1);
+ points[0] = Geom::Point(0, swidth);
+ //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse();
+ points[0][Geom::X] = (double)curve_length;
+ spdc_apply_powerstroke_shape(points, dc, item);
+
+ shape_applied = true;
+ break;
+ }
+ case ELLIPSE:
+ {
+ // "ellipse"
+ SPCurve *c = new SPCurve();
+ const double C1 = 0.552;
+ c->moveto(0, SHAPE_HEIGHT/2);
+ c->curveto(0, (1 - C1) * SHAPE_HEIGHT/2, (1 - C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH/2, 0);
+ c->curveto((1 + C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH, (1 - C1) * SHAPE_HEIGHT/2, SHAPE_LENGTH, SHAPE_HEIGHT/2);
+ c->curveto(SHAPE_LENGTH, (1 + C1) * SHAPE_HEIGHT/2, (1 + C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, SHAPE_LENGTH/2, SHAPE_HEIGHT);
+ c->curveto((1 - C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, 0, (1 + C1) * SHAPE_HEIGHT/2, 0, SHAPE_HEIGHT/2);
+ c->closepath();
+ spdc_paste_curve_as_freehand_shape(c->get_pathvector(), dc, item);
+ c->unref();
+
+ shape_applied = true;
+ break;
+ }
+ case CLIPBOARD:
+ {
+ // take shape from clipboard;
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ if(cm->paste(SP_ACTIVE_DESKTOP,true)){
+ SPItem * pasted_clipboard = dc->selection->singleItem();
+ dc->selection->toCurves();
+ pasted_clipboard = dc->selection->singleItem();
+ if(pasted_clipboard){
+ Inkscape::XML::Node *pasted_clipboard_root = pasted_clipboard->getRepr();
+ Inkscape::XML::Node *path = sp_repr_lookup_name(pasted_clipboard_root, "svg:path", -1); // unlimited search depth
+ if ( path != nullptr ) {
+ gchar const *svgd = path->attribute("d");
+ dc->selection->remove(SP_OBJECT(pasted_clipboard));
+ previous_shape_pathv = sp_svg_read_pathv(svgd);
+ previous_shape_pathv *= pasted_clipboard->transform;
+ spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item);
+
+ shape = CLIPBOARD;
+ shape_applied = true;
+ pasted_clipboard->deleteObject();
+ } else {
+ shape = NONE;
+ }
+ } else {
+ shape = NONE;
+ }
+ } else {
+ shape = NONE;
+ }
+ break;
+ }
+ case BEND_CLIPBOARD:
+ {
+ gchar const *svgd = item->getRepr()->attribute("d");
+ if(bend_item && (SP_IS_SHAPE(bend_item) || SP_IS_GROUP(bend_item))){
+ // If item is a SPRect, convert it to path first:
+ if ( dynamic_cast<SPRect *>(bend_item) ) {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop) {
+ Inkscape::Selection *sel = desktop->getSelection();
+ if ( sel && !sel->isEmpty() ) {
+ sel->clear();
+ sel->add(bend_item);
+ sel->toCurves();
+ bend_item = sel->singleItem();
+ }
+ }
+ }
+ bend_item->moveTo(item,false);
+ bend_item->transform.setTranslation(Geom::Point());
+ spdc_apply_bend_shape(svgd, dc, bend_item);
+ dc->selection->add(SP_OBJECT(bend_item));
+
+ shape = BEND_CLIPBOARD;
+ } else {
+ bend_item = nullptr;
+ shape = NONE;
+ }
+ break;
+ }
+ case LAST_APPLIED:
+ {
+ if(previous_shape_type == CLIPBOARD){
+ if(previous_shape_pathv.size() != 0){
+ spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item);
+ shape_applied = true;
+ shape = CLIPBOARD;
+ } else{
+ shape = NONE;
+ }
+ } else {
+ if(bend_item != nullptr && bend_item->getRepr() != nullptr){
+ gchar const *svgd = item->getRepr()->attribute("d");
+ dc->selection->add(SP_OBJECT(bend_item));
+ dc->selection->duplicate();
+ dc->selection->remove(SP_OBJECT(bend_item));
+ bend_item = dc->selection->singleItem();
+ if(bend_item){
+ bend_item->moveTo(item,false);
+ Geom::Coord expansion_X = bend_item->transform.expansionX();
+ Geom::Coord expansion_Y = bend_item->transform.expansionY();
+ bend_item->transform = Geom::Affine(1,0,0,1,0,0);
+ bend_item->transform.setExpansionX(expansion_X);
+ bend_item->transform.setExpansionY(expansion_Y);
+ spdc_apply_bend_shape(svgd, dc, bend_item);
+ dc->selection->add(SP_OBJECT(bend_item));
+
+ shape = BEND_CLIPBOARD;
+ } else {
+ shape = NONE;
+ }
+ } else {
+ shape = NONE;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ previous_shape_type = shape;
+
+ if (shape_applied) {
+ // apply original stroke color as fill and unset stroke; then return
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (!strcmp(cfill, "none")) {
+ sp_repr_css_set_property (css, "fill", cstroke);
+ } else {
+ sp_repr_css_set_property (css, "fill", cfill);
+ }
+ sp_repr_css_set_property (css, "stroke", "none");
+ sp_desktop_apply_css_recursive(dc->white_item, css, true);
+ sp_repr_css_attr_unref(css);
+ return;
+ }
+ if (dc->waiting_LPE_type != INVALID_LPE) {
+ Effect::createAndApply(dc->waiting_LPE_type, dc->desktop->doc(), item);
+ dc->waiting_LPE_type = INVALID_LPE;
+
+ if (SP_IS_LPETOOL_CONTEXT(dc)) {
+ // since a geometric LPE was applied, we switch back to "inactive" mode
+ lpetool_context_switch_mode(SP_LPETOOL_CONTEXT(dc), INVALID_LPE);
+ }
+ }
+ if (SP_IS_PEN_CONTEXT(dc)) {
+ SP_PEN_CONTEXT(dc)->setPolylineMode();
+ }
+ }
+}
+
+/*
+ * Selection handlers
+ */
+
+static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc)
+{
+ if (dc->attach) {
+ spdc_attach_selection(dc, sel);
+ }
+}
+
+/* fixme: We have to ensure this is not delayed (Lauris) */
+
+static void spdc_selection_modified(Inkscape::Selection *sel, guint /*flags*/, FreehandBase *dc)
+{
+ if (dc->attach) {
+ spdc_attach_selection(dc, sel);
+ }
+}
+
+static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection */*sel*/)
+{
+ // We reset white and forget white/start/end anchors
+ spdc_reset_white(dc);
+ dc->sa = nullptr;
+ dc->ea = nullptr;
+
+ SPItem *item = dc->selection ? dc->selection->singleItem() : nullptr;
+
+ if ( item && SP_IS_PATH(item) ) {
+ // Create new white data
+ // Item
+ dc->white_item = item;
+
+ // Curve list
+ // We keep it in desktop coordinates to eliminate calculation errors
+ SPCurve *norm = SP_PATH(item)->getCurveForEdit();
+ norm->transform((dc->white_item)->i2dt_affine());
+ g_return_if_fail( norm != nullptr );
+ dc->white_curves = norm->split();
+ norm->unref();
+
+ // Anchor list
+ for (auto c:dc->white_curves) {
+ g_return_if_fail( c->get_segment_count() > 0 );
+ if ( !c->is_closed() ) {
+ SPDrawAnchor *a;
+ a = sp_draw_anchor_new(dc, c, TRUE, *(c->first_point()));
+ if (a)
+ dc->white_anchors.push_back(a);
+ a = sp_draw_anchor_new(dc, c, FALSE, *(c->last_point()));
+ if (a)
+ dc->white_anchors.push_back(a);
+ }
+ }
+ // fixme: recalculate active anchor?
+ }
+}
+
+
+void spdc_endpoint_snap_rotation(ToolBase const *const ec, Geom::Point &p, Geom::Point const &o,
+ guint state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ unsigned const snaps = abs(prefs->getInt("/options/rotationsnapsperpi/value", 12));
+
+ SnapManager &m = ec->desktop->namedview->snap_manager;
+ m.setup(ec->desktop);
+
+ bool snap_enabled = m.snapprefs.getSnapEnabledGlobally();
+ if (state & GDK_SHIFT_MASK) {
+ // SHIFT disables all snapping, except the angular snapping. After all, the user explicitly asked for angular
+ // snapping by pressing CTRL, otherwise we wouldn't have arrived here. But although we temporarily disable
+ // the snapping here, we must still call for a constrained snap in order to apply the constraints (i.e. round
+ // to the nearest angle increment)
+ m.snapprefs.setSnapEnabledGlobally(false);
+ }
+
+ Inkscape::SnappedPoint dummy = m.constrainedAngularSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE), boost::optional<Geom::Point>(), o, snaps);
+ p = dummy.getPoint();
+
+ if (state & GDK_SHIFT_MASK) {
+ m.snapprefs.setSnapEnabledGlobally(snap_enabled); // restore the original setting
+ }
+
+ m.unSetup();
+}
+
+
+void spdc_endpoint_snap_free(ToolBase const * const ec, Geom::Point& p, boost::optional<Geom::Point> &start_of_line, guint const /*state*/)
+{
+ SPDesktop *dt = ec->desktop;
+ SnapManager &m = dt->namedview->snap_manager;
+ Inkscape::Selection *selection = dt->getSelection();
+
+ // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping)
+ // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment
+
+ m.setup(dt, true, selection->singleItem());
+ Inkscape::SnapCandidatePoint scp(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ if (start_of_line) {
+ scp.addOrigin(*start_of_line);
+ }
+
+ Inkscape::SnappedPoint sp = m.freeSnap(scp);
+ p = sp.getPoint();
+
+ m.unSetup();
+}
+
+static SPCurve *reverse_then_unref(SPCurve *orig)
+{
+ SPCurve *ret = orig->create_reverse();
+ orig->unref();
+ return ret;
+}
+
+void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed)
+{
+ // Concat RBG
+ SPCurve *c = dc->green_curve;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // Green
+ dc->green_curve = new SPCurve();
+ for (auto i : dc->green_bpaths)
+ sp_canvas_item_destroy(i);
+ dc->green_bpaths.clear();
+
+ // Blue
+ c->append_continuous(dc->blue_curve, 0.0625);
+ dc->blue_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->blue_bpath), nullptr);
+
+ // Red
+ if (dc->red_curve_is_valid) {
+ c->append_continuous(dc->red_curve, 0.0625);
+ }
+ dc->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->red_bpath), nullptr);
+
+ if (c->is_empty()) {
+ c->unref();
+ return;
+ }
+
+ // Step A - test, whether we ended on green anchor
+ if ( (forceclosed &&
+ (!dc->sa || (dc->sa && dc->sa->curve->is_empty()))) ||
+ ( dc->green_anchor && dc->green_anchor->active))
+ {
+ // We hit green anchor, closing Green-Blue-Red
+ dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ // Closed path, just flush
+ spdc_flush_white(dc, c);
+ c->unref();
+ return;
+ }
+
+ // Step B - both start and end anchored to same curve
+ if ( dc->sa && dc->ea
+ && ( dc->sa->curve == dc->ea->curve )
+ && ( ( dc->sa != dc->ea )
+ || dc->sa->curve->is_closed() ) )
+ {
+ // We hit bot start and end of single curve, closing paths
+ dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Closing path."));
+ dc->sa_overwrited->append_continuous(c, 0.0625);
+ c->unref();
+ dc->sa_overwrited->closepath_current();
+ if (!dc->white_curves.empty()) {
+ dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve));
+ }
+ dc->white_curves.push_back(dc->sa_overwrited);
+ spdc_flush_white(dc, nullptr);
+ return;
+ }
+ // Step C - test start
+ if (dc->sa) {
+ if (!dc->white_curves.empty()) {
+ dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve));
+ }
+ SPCurve *s = dc->sa_overwrited;
+ s->append_continuous(c, 0.0625);
+ c->unref();
+ c = s->ref();
+ } else /* Step D - test end */ if (dc->ea) {
+ SPCurve *e = dc->ea->curve;
+ if (!dc->white_curves.empty()) {
+ dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), e));
+ }
+ if (!dc->ea->start) {
+ e = reverse_then_unref(e);
+ }
+ if(prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1 ||
+ prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2){
+ e = reverse_then_unref(e);
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment());
+ SPCurve *lastSeg = new SPCurve();
+ if(cubic){
+ lastSeg->moveto((*cubic)[0]);
+ lastSeg->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]);
+ if( e->get_segment_count() == 1){
+ e = lastSeg;
+ }else{
+ //we eliminate the last segment
+ e->backspace();
+ //and we add it again with the recreation
+ e->append_continuous(lastSeg, 0.0625);
+ }
+ }
+ e = reverse_then_unref(e);
+ }
+ c->append_continuous(e, 0.0625);
+ e->unref();
+ }
+ if (forceclosed)
+ {
+ dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ }
+ spdc_flush_white(dc, c);
+
+ c->unref();
+}
+
+static void spdc_flush_white(FreehandBase *dc, SPCurve *gc)
+{
+ SPCurve *c;
+ if (! dc->white_curves.empty()) {
+ g_assert(dc->white_item);
+ c = SPCurve::concat(dc->white_curves);
+ dc->white_curves.clear();
+ if (gc) {
+ c->append(gc, FALSE);
+ }
+ } else if (gc) {
+ c = gc;
+ c->ref();
+ } else {
+ return;
+ }
+
+ // Now we have to go back to item coordinates at last
+ c->transform( dc->white_item
+ ? (dc->white_item)->dt2i_affine()
+ : dc->desktop->dt2doc() );
+
+ SPDesktop *desktop = dc->desktop;
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ if ( c && !c->is_empty() ) {
+ // We actually have something to write
+
+ bool has_lpe = false;
+ Inkscape::XML::Node *repr;
+
+ if (dc->white_item) {
+ repr = dc->white_item->getRepr();
+ has_lpe = SP_LPE_ITEM(dc->white_item)->hasPathEffectRecursive();
+ } else {
+ repr = xml_doc->createElement("svg:path");
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, tool_name(dc).data(), false);
+ }
+
+ gchar *str = sp_svg_write_path( c->get_pathvector() );
+ g_assert( str != nullptr );
+ if (has_lpe)
+ repr->setAttribute("inkscape:original-d", str);
+ else
+ repr->setAttribute("d", str);
+ g_free(str);
+
+ if (SP_IS_PENCIL_CONTEXT(dc) && dc->tablet_enabled) {
+ if (!dc->white_item) {
+ dc->white_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));
+ }
+ spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c, false);
+ }
+ if (!dc->white_item) {
+ // Attach repr
+ SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));
+ dc->white_item = item;
+ //Bend needs the transforms applied after, Other effects best before
+ spdc_check_for_and_apply_waiting_LPE(dc, item, c, true);
+ Inkscape::GC::release(repr);
+ item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ item->updateRepr();
+ item->doWriteTransform(item->transform, nullptr, true);
+ spdc_check_for_and_apply_waiting_LPE(dc, item, c, false);
+ dc->selection->set(repr);
+ if(previous_shape_type == BEND_CLIPBOARD){
+ repr->parent()->removeChild(repr);
+ }
+ }
+ DocumentUndo::done(doc, SP_IS_PEN_CONTEXT(dc)? SP_VERB_CONTEXT_PEN : SP_VERB_CONTEXT_PENCIL,
+ _("Draw path"));
+
+ // When quickly drawing several subpaths with Shift, the next subpath may be finished and
+ // flushed before the selection_modified signal is fired by the previous change, which
+ // results in the tool losing all of the selected path's curve except that last subpath. To
+ // fix this, we force the selection_modified callback now, to make sure the tool's curve is
+ // in sync immediately.
+ spdc_selection_modified(desktop->getSelection(), 0, dc);
+ }
+
+ c->unref();
+
+ // Flush pending updates
+ doc->ensureUpToDate();
+}
+
+SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p)
+{
+ SPDrawAnchor *active = nullptr;
+
+ // Test green anchor
+ if (dc->green_anchor) {
+ active = sp_draw_anchor_test(dc->green_anchor, p, TRUE);
+ }
+
+ for (auto i:dc->white_anchors) {
+ SPDrawAnchor *na = sp_draw_anchor_test(i, p, !active);
+ if ( !active && na ) {
+ active = na;
+ }
+ }
+ return active;
+}
+
+static void spdc_reset_white(FreehandBase *dc)
+{
+ if (dc->white_item) {
+ // We do not hold refcount
+ dc->white_item = nullptr;
+ }
+ for (auto i: dc->white_curves)
+ i->unref();
+ dc->white_curves.clear();
+ for (auto i:dc->white_anchors)
+ sp_draw_anchor_destroy(i);
+ dc->white_anchors.clear();
+}
+
+static void spdc_free_colors(FreehandBase *dc)
+{
+ // Red
+ if (dc->red_bpath) {
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->red_bpath));
+ dc->red_bpath = nullptr;
+ }
+ if (dc->red_curve) {
+ dc->red_curve = dc->red_curve->unref();
+ }
+
+ // Blue
+ if (dc->blue_bpath) {
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->blue_bpath));
+ dc->blue_bpath = nullptr;
+ }
+ if (dc->blue_curve) {
+ dc->blue_curve = dc->blue_curve->unref();
+ }
+
+ // Overwrite start anchor curve
+ if (dc->sa_overwrited) {
+ dc->sa_overwrited = dc->sa_overwrited->unref();
+ }
+ // Green
+ for (auto i : dc->green_bpaths)
+ sp_canvas_item_destroy(i);
+ dc->green_bpaths.clear();
+ if (dc->green_curve) {
+ dc->green_curve = dc->green_curve->unref();
+ }
+ if (dc->green_anchor) {
+ dc->green_anchor = sp_draw_anchor_destroy(dc->green_anchor);
+ }
+
+ // White
+ if (dc->white_item) {
+ // We do not hold refcount
+ dc->white_item = nullptr;
+ }
+ for (auto i: dc->white_curves)
+ i->unref();
+ dc->white_curves.clear();
+ for (auto i:dc->white_anchors)
+ sp_draw_anchor_destroy(i);
+ dc->white_anchors.clear();
+}
+
+void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state) {
+ g_return_if_fail(!strcmp(tool, "/tools/freehand/pen") || !strcmp(tool, "/tools/freehand/pencil")
+ || !strcmp(tool, "/tools/calligraphic") );
+ Glib::ustring tool_path = tool;
+
+ SPDesktop *desktop = ec->desktop;
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ repr->setAttribute("sodipodi:type", "arc");
+ SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));
+ item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ Inkscape::GC::release(repr);
+
+ // apply the tool's current style
+ sp_desktop_apply_style_tool(desktop, repr, tool, false);
+
+ // find out stroke width (TODO: is there an easier way??)
+ double stroke_width = 3.0;
+ gchar const *style_str = repr->attribute("style");
+ if (style_str) {
+ SPStyle style(SP_ACTIVE_DOCUMENT);
+ style.mergeString(style_str);
+ stroke_width = style.stroke_width.computed;
+ }
+
+ // unset stroke and set fill color to former stroke color
+ gchar * str;
+ str = strcmp(tool, "/tools/calligraphic") ? g_strdup_printf("fill:#%06x;stroke:none;", sp_desktop_get_color_tool(desktop, tool, false) >> 8)
+ : g_strdup_printf("fill:#%06x;stroke:#%06x;", sp_desktop_get_color_tool(desktop, tool, true) >> 8, sp_desktop_get_color_tool(desktop, tool, false) >> 8);
+ repr->setAttribute("style", str);
+ g_free(str);
+
+ // put the circle where the mouse click occurred and set the diameter to the
+ // current stroke width, multiplied by the amount specified in the preferences
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ Geom::Affine const i2d (item->i2dt_affine ());
+ Geom::Point pp = pt * i2d.inverse();
+
+ double rad = 0.5 * prefs->getDouble(tool_path + "/dot-size", 3.0);
+ if (!strcmp(tool, "/tools/calligraphic"))
+ rad = 0.0333 * prefs->getDouble(tool_path + "/width", 3.0) / desktop->current_zoom() / desktop->getDocument()->getDocumentScale()[Geom::X];
+ if (event_state & GDK_MOD1_MASK) {
+ // TODO: We vary the dot size between 0.5*rad and 1.5*rad, where rad is the dot size
+ // as specified in prefs. Very simple, but it might be sufficient in practice. If not,
+ // we need to devise something more sophisticated.
+ double s = g_random_double_range(-0.5, 0.5);
+ rad *= (1 + s);
+ }
+ if (event_state & GDK_SHIFT_MASK) {
+ // double the point size
+ rad *= 2;
+ }
+
+ sp_repr_set_svg_double (repr, "sodipodi:cx", pp[Geom::X]);
+ sp_repr_set_svg_double (repr, "sodipodi:cy", pp[Geom::Y]);
+ sp_repr_set_svg_double (repr, "sodipodi:rx", rad * stroke_width);
+ sp_repr_set_svg_double (repr, "sodipodi:ry", rad * stroke_width);
+ item->updateRepr();
+ item->doWriteTransform(item->transform, nullptr, true);
+
+ desktop->getSelection()->set(item);
+
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating single dot"));
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, _("Create single dot"));
+}
+
+}
+}
+}
+
+/*
+ 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/tools/freehand-base.h b/src/ui/tools/freehand-base.h
new file mode 100644
index 0000000..7dc960a
--- /dev/null
+++ b/src/ui/tools/freehand-base.h
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_DRAW_CONTEXT_H
+#define SEEN_SP_DRAW_CONTEXT_H
+
+/*
+ * Generic drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+#include "live_effects/effect-enum.h"
+
+struct SPCanvasItem;
+class SPCurve;
+struct SPDrawAnchor;
+
+namespace Inkscape {
+ class Selection;
+}
+
+namespace boost {
+ template<class T>
+ class optional;
+}
+
+/* Freehand context */
+
+#define SP_DRAW_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FreehandBase*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_DRAW_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FreehandBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class FreehandBase : public ToolBase {
+public:
+ FreehandBase(gchar const *const *cursor_shape);
+ ~FreehandBase() override;
+
+ Inkscape::Selection *selection;
+ SPCanvasItem *grab;
+
+ bool attach;
+
+ guint32 red_color;
+ guint32 blue_color;
+ guint32 green_color;
+ guint32 highlight_color;
+
+ // Red
+ SPCanvasItem *red_bpath;
+ SPCurve *red_curve;
+
+ // Blue
+ SPCanvasItem *blue_bpath;
+ SPCurve *blue_curve;
+
+ // Green
+ std::vector<SPCanvasItem*> green_bpaths;
+ SPCurve *green_curve;
+ SPDrawAnchor *green_anchor;
+ gboolean green_closed; // a flag meaning we hit the green anchor, so close the path on itself
+
+ // White
+ SPItem *white_item;
+ std::list<SPCurve *> white_curves;
+ std::vector<SPDrawAnchor*> white_anchors;
+
+ // Temporary modified curve when start anchor
+ SPCurve *sa_overwrited;
+
+ // Start anchor
+ SPDrawAnchor *sa;
+
+ // End anchor
+ SPDrawAnchor *ea;
+
+
+ /* type of the LPE that is to be applied automatically to a finished path (if any) */
+ Inkscape::LivePathEffect::EffectType waiting_LPE_type;
+
+ sigc::connection sel_changed_connection;
+ sigc::connection sel_modified_connection;
+
+ bool red_curve_is_valid;
+
+ bool anchor_statusbar;
+
+ bool tablet_enabled;
+
+ bool is_tablet;
+
+ gdouble pressure;
+ void set(const Inkscape::Preferences::Entry& val) override;
+
+protected:
+
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+};
+
+/**
+ * Returns FIRST active anchor (the activated one).
+ */
+SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p);
+
+/**
+ * Concats red, blue and green.
+ * If any anchors are defined, process these, optionally removing curves from white list
+ * Invoke _flush_white to write result back to object.
+ */
+void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed);
+
+/**
+ * Snaps node or handle to PI/rotationsnapsperpi degree increments.
+ *
+ * @param dc draw context.
+ * @param p cursor point (to be changed by snapping).
+ * @param o origin point.
+ * @param state keyboard state to check if ctrl or shift was pressed.
+ */
+void spdc_endpoint_snap_rotation(ToolBase const *const ec, Geom::Point &p, Geom::Point const &o, guint state);
+
+void spdc_endpoint_snap_free(ToolBase const *ec, Geom::Point &p, boost::optional<Geom::Point> &start_of_line, guint state);
+
+/**
+ * If we have an item and a waiting LPE, apply the effect to the item
+ * (spiro spline mode is treated separately).
+ */
+void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item);
+
+/**
+ * Create a single dot represented by a circle.
+ */
+void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state);
+
+}
+}
+}
+
+#endif // SEEN_SP_DRAW_CONTEXT_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/tools/gradient-tool.cpp b/src/ui/tools/gradient-tool.cpp
new file mode 100644
index 0000000..6cb00fc
--- /dev/null
+++ b/src/ui/tools/gradient-tool.cpp
@@ -0,0 +1,932 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient drawing and editing tool
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "gradient-drag.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+#include "verbs.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-stop.h"
+
+#include "display/sp-ctrlline.h"
+
+#include "ui/pixmaps/cursor-gradient-add.xpm"
+#include "ui/pixmaps/cursor-gradient.xpm"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/tools/gradient-tool.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint state, guint32 etime);
+
+const std::string& GradientTool::getPrefsPath() {
+ return GradientTool::prefsPath;
+}
+
+const std::string GradientTool::prefsPath = "/tools/gradient";
+
+
+GradientTool::GradientTool()
+ : ToolBase(cursor_gradient_xpm)
+ , cursor_addnode(false)
+ , node_added(false)
+// TODO: Why are these connections stored as pointers?
+ , selcon(nullptr)
+ , subselcon(nullptr)
+{
+ // TODO: This value is overwritten in the root handler
+ this->tolerance = 6;
+}
+
+GradientTool::~GradientTool() {
+ this->enableGrDrag(false);
+
+ this->selcon->disconnect();
+ delete this->selcon;
+
+ this->subselcon->disconnect();
+ delete this->subselcon;
+}
+
+// This must match GrPointType enum sp-gradient.h
+// We should move this to a shared header (can't simply move to gradient.h since that would require
+// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!).
+const gchar *gr_handle_descr [] = {
+ N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN
+ N_("Linear gradient <b>end</b>"),
+ N_("Linear gradient <b>mid stop</b>"),
+ N_("Radial gradient <b>center</b>"),
+ N_("Radial gradient <b>radius</b>"),
+ N_("Radial gradient <b>radius</b>"),
+ N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS
+ N_("Radial gradient <b>mid stop</b>"),
+ N_("Radial gradient <b>mid stop</b>"),
+ N_("Mesh gradient <b>corner</b>"),
+ N_("Mesh gradient <b>handle</b>"),
+ N_("Mesh gradient <b>tensor</b>")
+};
+
+void GradientTool::selection_changed(Inkscape::Selection*) {
+ GradientTool *rc = (GradientTool *) this;
+
+ GrDrag *drag = rc->_grdrag;
+ Inkscape::Selection *selection = this->desktop->getSelection();
+ if (selection == nullptr) {
+ return;
+ }
+ guint n_obj = (guint) boost::distance(selection->items());
+
+ if (!drag->isNonEmpty() || selection->isEmpty())
+ return;
+ guint n_tot = drag->numDraggers();
+ guint n_sel = drag->numSelected();
+
+ //The use of ngettext in the following code is intentional even if the English singular form would never be used
+ if (n_sel == 1) {
+ if (drag->singleSelectedDraggerNumDraggables() == 1) {
+ gchar * message = g_strconcat(
+ //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message
+ _("%s selected"),
+ //TRANSLATORS: Mind the space in front. This is part of a compound message
+ ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot),
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ rc->message_context->setF(Inkscape::NORMAL_MESSAGE,
+ message,_(gr_handle_descr[drag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj);
+ } else {
+ gchar * message = g_strconcat(
+ //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count)
+ ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected",
+ "One handle merging %d stops (drag with <b>Shift</b> to separate) selected",drag->singleSelectedDraggerNumDraggables()),
+ ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot),
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ rc->message_context->setF(Inkscape::NORMAL_MESSAGE,message,drag->singleSelectedDraggerNumDraggables(), n_tot, n_obj);
+ }
+ } else if (n_sel > 1) {
+ //TRANSLATORS: The plural refers to number of selected gradient handles. This is part of a compound message (part two indicates selected object count)
+ gchar * message = g_strconcat(ngettext("<b>%d</b> gradient handle selected out of %d","<b>%d</b> gradient handles selected out of %d",n_sel),
+ //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ rc->message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj);
+ } else if (n_sel == 0) {
+ rc->message_context->setF(Inkscape::NORMAL_MESSAGE,
+ //TRANSLATORS: The plural refers to number of selected objects
+ ngettext("<b>No</b> gradient handles selected out of %d on %d selected object",
+ "<b>No</b> gradient handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj);
+ }
+}
+
+void GradientTool::setup() {
+ ToolBase::setup();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/gradient/selcue", true)) {
+ this->enableSelectionCue();
+ }
+
+ this->enableGrDrag();
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ this->selcon = new sigc::connection(selection->connectChanged(
+ sigc::mem_fun(this, &GradientTool::selection_changed)
+ ));
+
+ this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged(
+ sigc::hide(sigc::bind(
+ sigc::mem_fun(this, &GradientTool::selection_changed),
+ (Inkscape::Selection*)nullptr
+ ))
+ ));
+
+ this->selection_changed(selection);
+}
+
+void
+sp_gradient_context_select_next (ToolBase *event_context)
+{
+ GrDrag *drag = event_context->_grdrag;
+ g_assert (drag);
+
+ GrDragger *d = drag->select_next();
+
+ event_context->desktop->scroll_to_point(d->point, 1.0);
+}
+
+void
+sp_gradient_context_select_prev (ToolBase *event_context)
+{
+ GrDrag *drag = event_context->_grdrag;
+ g_assert (drag);
+
+ GrDragger *d = drag->select_prev();
+
+ event_context->desktop->scroll_to_point(d->point, 1.0);
+}
+
+static bool
+sp_gradient_context_is_over_line (GradientTool *rc, SPItem *item, Geom::Point event_p)
+{
+ SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop;
+
+ //Translate mouse point into proper coord system
+ rc->mousepoint_doc = desktop->w2d(event_p);
+
+ if (SP_IS_CTRLLINE(item)) {
+ SPCtrlLine* line = SP_CTRLLINE(item);
+
+ Geom::LineSegment ls(line->s, line->e);
+ Geom::Point nearest = ls.pointAt(ls.nearestTime(rc->mousepoint_doc));
+ double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom();
+
+ double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance;
+
+ bool close = (dist_screen < tolerance);
+
+ return close;
+ }
+ return false;
+}
+
+static std::vector<Geom::Point>
+sp_gradient_context_get_stop_intervals (GrDrag *drag, std::vector<SPStop *> &these_stops, std::vector<SPStop *> &next_stops)
+{
+ std::vector<Geom::Point> coords;
+
+ // for all selected draggers
+ for (std::set<GrDragger *>::const_iterator i = drag->selected.begin(); i != drag->selected.end() ; ++i ) {
+ GrDragger *dragger = *i;
+ // remember the coord of the dragger to reselect it later
+ coords.push_back(dragger->point);
+ // for all draggables of dragger
+ for (std::vector<GrDraggable *>::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j) {
+ GrDraggable *d = *j;
+
+ // find the gradient
+ SPGradient *gradient = getGradient(d->item, d->fill_or_stroke);
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false);
+
+ // these draggable types cannot have a next draggabe to insert a stop between them
+ if (d->point_type == POINT_LG_END ||
+ d->point_type == POINT_RG_FOCUS ||
+ d->point_type == POINT_RG_R1 ||
+ d->point_type == POINT_RG_R2) {
+ continue;
+ }
+
+ // from draggables to stops
+ SPStop *this_stop = sp_get_stop_i (vector, d->point_i);
+ SPStop *next_stop = this_stop->getNextStop();
+ SPStop *last_stop = sp_last_stop (vector);
+
+ Inkscape::PaintTarget fs = d->fill_or_stroke;
+ SPItem *item = d->item;
+ gint type = d->point_type;
+ gint p_i = d->point_i;
+
+ // if there's a next stop,
+ if (next_stop) {
+ GrDragger *dnext = nullptr;
+ // find its dragger
+ // (complex because it may have different types, and because in radial,
+ // more than one dragger may correspond to a stop, so we must distinguish)
+ if (type == POINT_LG_BEGIN || type == POINT_LG_MID) {
+ if (next_stop == last_stop) {
+ dnext = drag->getDraggerFor(item, POINT_LG_END, p_i+1, fs);
+ } else {
+ dnext = drag->getDraggerFor(item, POINT_LG_MID, p_i+1, fs);
+ }
+ } else { // radial
+ if (type == POINT_RG_CENTER || type == POINT_RG_MID1) {
+ if (next_stop == last_stop) {
+ dnext = drag->getDraggerFor(item, POINT_RG_R1, p_i+1, fs);
+ } else {
+ dnext = drag->getDraggerFor(item, POINT_RG_MID1, p_i+1, fs);
+ }
+ }
+ if ((type == POINT_RG_MID2) ||
+ (type == POINT_RG_CENTER && dnext && !dnext->isSelected())) {
+ if (next_stop == last_stop) {
+ dnext = drag->getDraggerFor(item, POINT_RG_R2, p_i+1, fs);
+ } else {
+ dnext = drag->getDraggerFor(item, POINT_RG_MID2, p_i+1, fs);
+ }
+ }
+ }
+
+ // if both adjacent draggers selected,
+ if ((std::find(these_stops.begin(),these_stops.end(),this_stop)==these_stops.end()) && dnext && dnext->isSelected()) {
+
+ // remember the coords of the future dragger to select it
+ coords.push_back(0.5*(dragger->point + dnext->point));
+
+ // do not insert a stop now, it will confuse the loop;
+ // just remember the stops
+ these_stops.push_back(this_stop);
+ next_stops.push_back(next_stop);
+ }
+ }
+ }
+ }
+ return coords;
+}
+
+void
+sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc)
+{
+ SPDocument *doc = nullptr;
+ GrDrag *drag = rc->_grdrag;
+
+ std::vector<SPStop *> these_stops;
+ std::vector<SPStop *> next_stops;
+
+ std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops);
+
+ if (these_stops.empty() && drag->numSelected() == 1) {
+ // if a single stop is selected, add between that stop and the next one
+ GrDragger *dragger = *(drag->selected.begin());
+ for (auto d : dragger->draggables) {
+ if (d->point_type == POINT_RG_FOCUS) {
+ /*
+ * There are 2 draggables at the center (start) of a radial gradient
+ * To avoid creating 2 separate stops, ignore this draggable point type
+ */
+ continue;
+ }
+ SPGradient *gradient = getGradient(d->item, d->fill_or_stroke);
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false);
+ SPStop *this_stop = sp_get_stop_i (vector, d->point_i);
+ if (this_stop) {
+ SPStop *next_stop = this_stop->getNextStop();
+ if (next_stop) {
+ these_stops.push_back(this_stop);
+ next_stops.push_back(next_stop);
+ }
+ }
+ }
+ }
+
+ // now actually create the new stops
+ auto i = these_stops.rbegin();
+ auto j = next_stops.rbegin();
+ std::vector<SPStop *> new_stops;
+
+ for (;i != these_stops.rend() && j != next_stops.rend(); ++i, ++j ) {
+ SPStop *this_stop = *i;
+ SPStop *next_stop = *j;
+ gfloat offset = 0.5*(this_stop->offset + next_stop->offset);
+ SPObject *parent = this_stop->parent;
+ if (SP_IS_GRADIENT (parent)) {
+ doc = parent->document;
+ SPStop *new_stop = sp_vector_add_stop (SP_GRADIENT (parent), this_stop, next_stop, offset);
+ new_stops.push_back(new_stop);
+ SP_GRADIENT(parent)->ensureVector();
+ }
+ }
+
+ if (!these_stops.empty() && doc) {
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Add gradient stop"));
+ drag->updateDraggers();
+ // so that it does not automatically update draggers in idle loop, as this would deselect
+ drag->local_change = true;
+
+ // select the newly created stops
+ for (auto i:new_stops) {
+ drag->selectByStop(i);
+ }
+ }
+}
+
+static double sqr(double x) {return x*x;}
+
+/**
+ * Remove unnecessary stops in the adjacent currently selected stops
+ *
+ * For selected stops that are adjacent to each other, remove
+ * stops that don't change the gradient visually, within a range of tolerance.
+ *
+ * @param rc GradientTool used to extract selected stops
+ * @param tolerance maximum difference between stop and expected color at that position
+ */
+static void
+sp_gradient_simplify(GradientTool *rc, double tolerance)
+{
+ SPDocument *doc = nullptr;
+ GrDrag *drag = rc->_grdrag;
+
+ std::vector<SPStop *> these_stops;
+ std::vector<SPStop *> next_stops;
+
+ std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops);
+
+ std::set<SPStop *> todel;
+
+ auto i = these_stops.begin();
+ auto j = next_stops.begin();
+ for (; i != these_stops.end() && j != next_stops.end(); ++i, ++j) {
+ SPStop *stop0 = *i;
+ SPStop *stop1 = *j;
+
+ // find the next adjacent stop if it exists and is in selection
+ auto i1 = std::find(these_stops.begin(), these_stops.end(), stop1);
+ if (i1 != these_stops.end()) {
+ if (next_stops.size()>(i1-these_stops.begin())) {
+ SPStop *stop2 = *(next_stops.begin() + (i1-these_stops.begin()));
+
+ if (todel.find(stop0)!=todel.end() || todel.find(stop2) != todel.end())
+ continue;
+
+ // compare color of stop1 to the average color of stop0 and stop2
+ guint32 const c0 = stop0->get_rgba32();
+ guint32 const c2 = stop2->get_rgba32();
+ guint32 const c1r = stop1->get_rgba32();
+ guint32 c1 = average_color (c0, c2,
+ (stop1->offset - stop0->offset) / (stop2->offset - stop0->offset));
+
+ double diff =
+ sqr(SP_RGBA32_R_F(c1) - SP_RGBA32_R_F(c1r)) +
+ sqr(SP_RGBA32_G_F(c1) - SP_RGBA32_G_F(c1r)) +
+ sqr(SP_RGBA32_B_F(c1) - SP_RGBA32_B_F(c1r)) +
+ sqr(SP_RGBA32_A_F(c1) - SP_RGBA32_A_F(c1r));
+
+ if (diff < tolerance)
+ todel.insert(stop1);
+ }
+ }
+ }
+
+ for (auto stop : todel) {
+ doc = stop->document;
+ Inkscape::XML::Node * parent = stop->getRepr()->parent();
+ parent->removeChild( stop->getRepr() );
+ }
+
+ if (!todel.empty()) {
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Simplify gradient"));
+ drag->local_change = true;
+ drag->updateDraggers();
+ drag->selectByCoords(coords);
+ }
+}
+
+
+static void
+sp_gradient_context_add_stop_near_point (GradientTool *rc, SPItem *item, Geom::Point mouse_p, guint32 /*etime*/)
+{
+ // item is the selected item. mouse_p the location in doc coordinates of where to add the stop
+
+ ToolBase *ec = SP_EVENT_CONTEXT(rc);
+ SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop;
+
+ double tolerance = (double) ec->tolerance;
+
+ SPStop *newstop = ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom());
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT,
+ _("Add gradient stop"));
+
+ ec->get_drag()->updateDraggers();
+ ec->get_drag()->local_change = true;
+ ec->get_drag()->selectByStop(newstop);
+}
+
+bool GradientTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+
+ GrDrag *drag = this->_grdrag;
+ g_assert (drag);
+
+ gint ret = FALSE;
+
+ auto move_handle = [&](int x_dir, int y_dir) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__SHIFT(event)) {
+ mul *= 10;
+ }
+
+ y_dir *= -desktop->yaxisdir();
+
+ if (MOD__ALT(event)) {
+ drag->selected_move_screen(mul * x_dir, mul * y_dir);
+ } else {
+ mul *= nudge;
+ drag->selected_move(mul * x_dir, mul * y_dir);
+ }
+ };
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if ( event->button.button == 1 ) {
+ bool over_line = false;
+ SPCtrlLine *line = nullptr;
+
+ if (!drag->lines.empty()) {
+ for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end() && (!over_line); ++l) {
+ line = *l;
+ over_line |= sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y));
+ }
+ }
+
+ if (over_line) {
+ // we take the first item in selection, because with doubleclick, the first click
+ // always resets selection to the single object under cursor
+ sp_gradient_context_add_stop_near_point(this, SP_ITEM(selection->items().front()), this->mousepoint_doc, event->button.time);
+ } else {
+ auto items= selection->items();
+ for (auto i = items.begin();i!=items.end();++i) {
+ SPItem *item = *i;
+ SPGradientType new_type = (SPGradientType) prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR);
+ Inkscape::PaintTarget fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+
+ SPGradient *vector = sp_gradient_vector_for_object(desktop->getDocument(), desktop, item, fsmode);
+
+ SPGradient *priv = sp_item_set_gradient(item, vector, new_type, fsmode);
+ sp_gradient_reset_to_userspace(priv, item);
+ }
+ desktop->redrawDesktop();;
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT,
+ _("Create default gradient"));
+ }
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1 && !this->space_panning ) {
+ Geom::Point button_w(event->button.x, event->button.y);
+
+ // save drag origin
+ this->xp = (gint) button_w[Geom::X];
+ this->yp = (gint) button_w[Geom::Y];
+ this->within_tolerance = true;
+
+ dragging = true;
+
+ Geom::Point button_dt = desktop->w2d(button_w);
+ if (event->button.state & GDK_SHIFT_MASK) {
+ Inkscape::Rubberband::get(desktop)->start(desktop, button_dt);
+ } else {
+ // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to
+ // enable Ctrl+doubleclick of exactly the selected item(s)
+ if (!(event->button.state & GDK_CONTROL_MASK)) {
+ this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+ }
+
+ if (!selection->isEmpty()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ }
+
+ this->origin = button_dt;
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point const motion_dt = this->desktop->w2d(motion_w);
+
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->move(motion_dt);
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them"));
+ } else {
+ sp_gradient_drag(*this, motion_dt, event->motion.state, event->motion.time);
+ }
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ ret = TRUE;
+ } else {
+ if (!drag->mouseOver() && !selection->isEmpty()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt = this->desktop->w2d(motion_w);
+
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE));
+ m.unSetup();
+ }
+
+ bool over_line = false;
+
+ if (!drag->lines.empty()) {
+ for (auto line : drag->lines) {
+ over_line |= sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y));
+ }
+ }
+
+ if (this->cursor_addnode && !over_line) {
+ this->cursor_shape = cursor_gradient_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_addnode = false;
+ } else if (!this->cursor_addnode && over_line) {
+ this->cursor_shape = cursor_gradient_add_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_addnode = true;
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+
+ if ( event->button.button == 1 && !this->space_panning ) {
+ bool over_line = false;
+ SPCtrlLine *line = nullptr;
+
+ if (!drag->lines.empty()) {
+ for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end() && (!over_line); ++l) {
+ line = *l;
+ over_line = sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y));
+ }
+ }
+
+ if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) {
+ if (over_line && line) {
+ sp_gradient_context_add_stop_near_point(this, line->item, this->mousepoint_doc, 0);
+ ret = TRUE;
+ }
+ } else {
+ dragging = false;
+
+ // unless clicked with Ctrl (to enable Ctrl+doubleclick).
+ if (event->button.state & GDK_CONTROL_MASK) {
+ ret = TRUE;
+ break;
+ }
+
+ if (!this->within_tolerance) {
+ // we've been dragging, either do nothing (grdrag handles that),
+ // or rubberband-select if we have rubberband
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+
+ if (r->is_started() && !this->within_tolerance) {
+ // this was a rubberband drag
+ if (r->getMode() == RUBBERBAND_MODE_RECT) {
+ Geom::OptRect const b = r->getRectangle();
+ drag->selectRect(*b);
+ }
+ }
+ } else if (this->item_to_select) {
+ if (over_line && line) {
+ // Clicked on an existing gradient line, don't change selection. This stops
+ // possible change in selection during a double click with overlapping objects
+ } else {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ drag->deselectAll();
+ selection->set(this->item_to_select);
+ }
+ }
+ } else {
+ // click in an empty space; do the same as Esc
+ if (!drag->selected.empty()) {
+ drag->deselectAll();
+ } else {
+ selection->clear();
+ }
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ }
+
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+ sp_event_show_modifier_tip (this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: snap gradient angle"),
+ _("<b>Shift</b>: draw gradient around the starting point"),
+ nullptr);
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("altx-grad");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_A:
+ case GDK_KEY_a:
+ if (MOD__CTRL_ONLY(event) && drag->isNonEmpty()) {
+ drag->selectAll();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_L:
+ case GDK_KEY_l:
+ if (MOD__CTRL_ONLY(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_gradient_simplify(this, 1e-4);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (!drag->selected.empty()) {
+ drag->deselectAll();
+ } else {
+ Inkscape::SelectionHelper::selectNone(desktop);
+ }
+ ret = TRUE;
+ //TODO: make dragging escapable by Esc
+ break;
+
+ case GDK_KEY_Left: // move handle left
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(-1, 0);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Up: // move handle up
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(0, 1);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Right: // move handle right
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(1, 0);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Down: // move handle down
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(0, -1);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_r:
+ case GDK_KEY_R:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_gradient_reverse_selected_gradients(desktop);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Insert:
+ case GDK_KEY_KP_Insert:
+ // with any modifiers:
+ sp_gradient_context_add_stops_between_selected_stops (this);
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_i:
+ case GDK_KEY_I:
+ if (MOD__SHIFT_ONLY(event)) {
+ // Shift+I - insert stops (alternate keybinding for keyboards
+ // that don't have the Insert key)
+ sp_gradient_context_add_stops_between_selected_stops (this);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+// Creates a new linear or radial gradient.
+static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint /*state*/, guint32 etime)
+{
+ SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+ ToolBase *ec = SP_EVENT_CONTEXT(&rc);
+
+ if (!selection->isEmpty()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int type = prefs->getInt("/tools/gradient/newgradient", 1);
+ Inkscape::PaintTarget fill_or_stroke = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+
+ SPGradient *vector;
+ if (ec->item_to_select) {
+ // pick color from the object where drag started
+ vector = sp_gradient_vector_for_object(document, desktop, ec->item_to_select, fill_or_stroke);
+ } else {
+ // Starting from empty space:
+ // Sort items so that the topmost comes last
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+ sort(items.begin(),items.end(),sp_item_repr_compare_position_bool);
+ // take topmost
+ vector = sp_gradient_vector_for_object(document, desktop, SP_ITEM(items.back()), fill_or_stroke);
+ }
+
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+
+ auto itemlist = selection->items();
+ for (auto i = itemlist.begin();i!=itemlist.end();++i) {
+
+ //FIXME: see above
+ sp_repr_css_change_recursive((*i)->getRepr(), css, "style");
+
+ sp_item_set_gradient(*i, vector, (SPGradientType) type, fill_or_stroke);
+
+ if (type == SP_GRADIENT_TYPE_LINEAR) {
+ sp_item_gradient_set_coords (*i, POINT_LG_BEGIN, 0, rc.origin, fill_or_stroke, true, false);
+ sp_item_gradient_set_coords (*i, POINT_LG_END, 0, pt, fill_or_stroke, true, false);
+ } else if (type == SP_GRADIENT_TYPE_RADIAL) {
+ sp_item_gradient_set_coords (*i, POINT_RG_CENTER, 0, rc.origin, fill_or_stroke, true, false);
+ sp_item_gradient_set_coords (*i, POINT_RG_R1, 0, pt, fill_or_stroke, true, false);
+ }
+ (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ }
+ if (ec->_grdrag) {
+ ec->_grdrag->updateDraggers();
+ // prevent regenerating draggers by selection modified signal, which sometimes
+ // comes too late and thus destroys the knot which we will now grab:
+ ec->_grdrag->local_change = true;
+ // give the grab out-of-bounds values of xp/yp because we're already dragging
+ // and therefore are already out of tolerance
+ ec->_grdrag->grabKnot (selection->items().front(),
+ type == SP_GRADIENT_TYPE_LINEAR? POINT_LG_END : POINT_RG_R1,
+ -1, // ignore number (though it is always 1)
+ fill_or_stroke, 99999, 99999, etime);
+ }
+ // We did an undoable action, but SPDocumentUndo::done will be called by the knot when released
+
+ // status text; we do not track coords because this branch is run once, not all the time
+ // during drag
+ int n_objects = (int) boost::distance(selection->items());
+ rc.message_context->setF(Inkscape::NORMAL_MESSAGE,
+ ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle",
+ "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects),
+ n_objects);
+ } else {
+ desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient."));
+ }
+}
+
+}
+}
+}
+
+
+/*
+ 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/tools/gradient-tool.h b/src/ui/tools/gradient-tool.h
new file mode 100644
index 0000000..5de281a
--- /dev/null
+++ b/src/ui/tools/gradient-tool.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_GRADIENT_CONTEXT_H__
+#define __SP_GRADIENT_CONTEXT_H__
+
+/*
+ * Gradient drawing and editing tool
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Jon A. Cruz <jon@joncruz.org.
+ *
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005,2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include "ui/tools/tool-base.h"
+
+#define SP_GRADIENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::GradientTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_GRADIENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::GradientTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class GradientTool : public ToolBase {
+public:
+ GradientTool();
+ ~GradientTool() override;
+
+ Geom::Point origin;
+
+ bool cursor_addnode;
+
+ bool node_added;
+
+ Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords
+
+ sigc::connection *selcon;
+ sigc::connection *subselcon;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ void selection_changed(Inkscape::Selection*);
+};
+
+void sp_gradient_context_select_next (ToolBase *event_context);
+void sp_gradient_context_select_prev (ToolBase *event_context);
+void sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc);
+
+}
+}
+}
+
+#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/tools/lpe-tool.cpp b/src/ui/tools/lpe-tool.cpp
new file mode 100644
index 0000000..d1e7e18
--- /dev/null
+++ b/src/ui/tools/lpe-tool.cpp
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs
+ *
+ * Authors:
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtk/gtk.h>
+
+#include <2geom/sbasis-geometric.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection.h"
+
+#include "display/curve.h"
+#include "display/canvas-bpath.h"
+#include "display/canvas-text.h"
+
+#include "object/sp-path.h"
+
+#include "ui/pixmaps/cursor-crosshairs.xpm"
+
+#include "util/units.h"
+
+#include "ui/toolbar/lpe-toolbar.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/shape-editor.h"
+
+using Inkscape::Util::unit_table;
+using Inkscape::UI::Tools::PenTool;
+
+const int num_subtools = 8;
+
+SubtoolEntry lpesubtools[] = {
+ // this must be here to account for the "all inactive" action
+ {Inkscape::LivePathEffect::INVALID_LPE, "draw-geometry-inactive"},
+ {Inkscape::LivePathEffect::LINE_SEGMENT, "draw-geometry-line-segment"},
+ {Inkscape::LivePathEffect::CIRCLE_3PTS, "draw-geometry-circle-from-three-points"},
+ {Inkscape::LivePathEffect::CIRCLE_WITH_RADIUS, "draw-geometry-circle-from-radius"},
+ {Inkscape::LivePathEffect::PARALLEL, "draw-geometry-line-parallel"},
+ {Inkscape::LivePathEffect::PERP_BISECTOR, "draw-geometry-line-perpendicular"},
+ {Inkscape::LivePathEffect::ANGLE_BISECTOR, "draw-geometry-angle-bisector"},
+ {Inkscape::LivePathEffect::MIRROR_SYMMETRY, "draw-geometry-mirror"}
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data);
+
+const std::string& LpeTool::getPrefsPath() {
+ return LpeTool::prefsPath;
+}
+
+const std::string LpeTool::prefsPath = "/tools/lpetool";
+
+LpeTool::LpeTool()
+ : PenTool(cursor_crosshairs_xpm)
+ , shape_editor(nullptr)
+ , canvas_bbox(nullptr)
+ , mode(Inkscape::LivePathEffect::BEND_PATH)
+// TODO: pointer?
+ , measuring_items(new std::map<SPPath *, SPCanvasItem*>)
+{
+}
+
+LpeTool::~LpeTool() {
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ if (this->canvas_bbox) {
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(this->canvas_bbox));
+ this->canvas_bbox = nullptr;
+ }
+
+ lpetool_delete_measuring_items(this);
+ delete this->measuring_items;
+ this->measuring_items = nullptr;
+
+ this->sel_changed_connection.disconnect();
+}
+
+void LpeTool::setup() {
+ PenTool::setup();
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+ SPItem *item = selection->singleItem();
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection =
+ selection->connectChanged(sigc::bind(sigc::ptr_fun(&sp_lpetool_context_selection_changed), (gpointer)this));
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ lpetool_context_switch_mode(this, Inkscape::LivePathEffect::INVALID_LPE);
+ lpetool_context_reset_limiting_bbox(this);
+ lpetool_create_measuring_items(this);
+
+// TODO temp force:
+ this->enableSelectionCue();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ if (prefs->getBool("/tools/lpetool/selcue")) {
+ this->enableSelectionCue();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new nodepath and reassigns listeners to the new selected item's repr.
+ */
+void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data)
+{
+ LpeTool *lc = SP_LPETOOL_CONTEXT(data);
+
+ lc->shape_editor->unset_item();
+ SPItem *item = selection->singleItem();
+ lc->shape_editor->set_item(item);
+}
+
+void LpeTool::set(const Inkscape::Preferences::Entry& val) {
+ if (val.getEntryName() == "mode") {
+ Inkscape::Preferences::get()->setString("/tools/geometric/mode", "drag");
+ SP_PEN_CONTEXT(this)->mode = PenTool::MODE_DRAG;
+ }
+}
+
+bool LpeTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ {
+ // select the clicked item but do nothing else
+ Inkscape::Selection * const selection = this->desktop->getSelection();
+ selection->clear();
+ selection->add(item);
+ ret = TRUE;
+ break;
+ }
+ case GDK_BUTTON_RELEASE:
+ // TODO: do we need to catch this or can we pass it on to the parent handler?
+ ret = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = PenTool::item_handler(item, event);
+ }
+
+ return ret;
+}
+
+bool LpeTool::root_handler(GdkEvent* event) {
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ bool ret = false;
+
+ if (this->hasWaitingLPE()) {
+ // quit when we are waiting for a LPE to be applied
+ //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event);
+ return PenTool::root_handler(event);
+ }
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ if (this->mode == Inkscape::LivePathEffect::INVALID_LPE) {
+ // don't do anything for now if we are inactive (except clearing the selection
+ // since this was a click into empty space)
+ selection->clear();
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Choose a construction tool from the toolbar."));
+ ret = true;
+ break;
+ }
+
+ // save drag origin
+ this->xp = (gint) event->button.x;
+ this->yp = (gint) event->button.y;
+ this->within_tolerance = true;
+
+ using namespace Inkscape::LivePathEffect;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int mode = prefs->getInt("/tools/lpetool/mode");
+ EffectType type = lpesubtools[mode].type;
+
+ //bool over_stroke = lc->shape_editor->is_over_stroke(Geom::Point(event->button.x, event->button.y), true);
+
+ this->waitForLPEMouseClicks(type, Inkscape::LivePathEffect::Effect::acceptsNumClicks(type));
+
+ // we pass the mouse click on to pen tool as the first click which it should collect
+ //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event);
+ ret = PenTool::root_handler(event);
+ }
+ break;
+
+
+ case GDK_BUTTON_RELEASE:
+ {
+ /**
+ break;
+ **/
+ }
+
+ case GDK_KEY_PRESS:
+ /**
+ switch (get_latin_keyval (&event->key)) {
+ }
+ break;
+ **/
+
+ case GDK_KEY_RELEASE:
+ /**
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_Control_L:
+ case GDK_Control_R:
+ dc->_message_context->clear();
+ break;
+ default:
+ break;
+ }
+ **/
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = PenTool::root_handler(event);
+ }
+
+ return ret;
+}
+
+/*
+ * Finds the index in the list of geometric subtools corresponding to the given LPE type.
+ * Returns -1 if no subtool is found.
+ */
+int
+lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type) {
+ for (int i = 0; i < num_subtools; ++i) {
+ if (lpesubtools[i].type == type) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/*
+ * Checks whether an item has a construction applied as LPE and if so returns the index in
+ * lpesubtools of this construction
+ */
+int lpetool_item_has_construction(LpeTool */*lc*/, SPItem *item)
+{
+ if (!SP_IS_LPE_ITEM(item)) {
+ return -1;
+ }
+
+ Inkscape::LivePathEffect::Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ if (!lpe) {
+ return -1;
+ }
+ return lpetool_mode_to_index(lpe->effectType());
+}
+
+/*
+ * Attempts to perform the construction of the given type (i.e., to apply the corresponding LPE) to
+ * a single selected item. Returns whether we succeeded.
+ */
+bool
+lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type)
+{
+ Inkscape::Selection *selection = lc->desktop->getSelection();
+ SPItem *item = selection->singleItem();
+
+ // TODO: should we check whether type represents a valid geometric construction?
+ if (item && SP_IS_LPE_ITEM(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) {
+ Inkscape::LivePathEffect::Effect::createAndApply(type, lc->desktop->getDocument(), item);
+ return true;
+ }
+ return false;
+}
+
+void
+lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type)
+{
+ int index = lpetool_mode_to_index(type);
+ if (index != -1) {
+ lc->mode = type;
+ auto tb = dynamic_cast<UI::Toolbar::LPEToolbar*>(lc->desktop->get_toolbar_by_name("LPEToolToolbar"));
+
+ if(tb) {
+ tb->set_mode(index);
+ } else {
+ std::cerr << "Could not access LPE toolbar" << std::endl;
+ }
+ } else {
+ g_warning ("Invalid mode selected: %d", type);
+ return;
+ }
+}
+
+void
+lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B) {
+ Geom::Coord w = document->getWidth().value("px");
+ Geom::Coord h = document->getHeight().value("px");
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ double ulx = prefs->getDouble("/tools/lpetool/bbox_upperleftx", 0);
+ double uly = prefs->getDouble("/tools/lpetool/bbox_upperlefty", 0);
+ double lrx = prefs->getDouble("/tools/lpetool/bbox_lowerrightx", w);
+ double lry = prefs->getDouble("/tools/lpetool/bbox_lowerrighty", h);
+
+ A = Geom::Point(ulx, uly);
+ B = Geom::Point(lrx, lry);
+}
+
+/*
+ * Reads the limiting bounding box from preferences and draws it on the screen
+ */
+// TODO: Note that currently the bbox is not user-settable; we simply use the page borders
+void
+lpetool_context_reset_limiting_bbox(LpeTool *lc)
+{
+ if (lc->canvas_bbox) {
+ sp_canvas_item_destroy(lc->canvas_bbox);
+ lc->canvas_bbox = nullptr;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/tools/lpetool/show_bbox", true))
+ return;
+
+ SPDocument *document = lc->desktop->getDocument();
+
+ Geom::Point A, B;
+ lpetool_get_limiting_bbox_corners(document, A, B);
+ Geom::Affine doc2dt(lc->desktop->doc2dt());
+ A *= doc2dt;
+ B *= doc2dt;
+
+ Geom::Rect rect(A, B);
+ SPCurve *curve = SPCurve::new_from_rect(rect);
+
+ lc->canvas_bbox = sp_canvas_bpath_new (lc->desktop->getControls(), curve);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(lc->canvas_bbox), 0x0000ffff, 0.8, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT, 5, 5);
+}
+
+static void
+set_pos_and_anchor(SPCanvasText *canvas_text, const Geom::Piecewise<Geom::D2<Geom::SBasis> > &pwd2,
+ const double t, const double length, bool /*use_curvature*/ = false)
+{
+ using namespace Geom;
+
+ Piecewise<D2<SBasis> > pwd2_reparam = arc_length_parametrization(pwd2, 2 , 0.1);
+ double t_reparam = pwd2_reparam.cuts.back() * t;
+ Point pos = pwd2_reparam.valueAt(t_reparam);
+ Point dir = unit_vector(derivative(pwd2_reparam).valueAt(t_reparam));
+ Point n = -rot90(dir);
+ double angle = Geom::angle_between(dir, Point(1,0));
+
+ sp_canvastext_set_coords(canvas_text, pos + n * length);
+ sp_canvastext_set_anchor_manually(canvas_text, std::sin(angle), -std::cos(angle));
+}
+
+void
+lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection)
+{
+ if (!selection) {
+ selection = lc->desktop->getSelection();
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show = prefs->getBool("/tools/lpetool/show_measuring_info", true);
+
+ SPPath *path;
+ SPCurve *curve;
+ SPCanvasText *canvas_text;
+ SPCanvasGroup *tmpgrp = lc->desktop->getTempGroup();
+ gchar *arc_length;
+ double lengthval;
+ auto items= selection->items();
+ for(auto i=items.begin();i!=items.end();++i){
+ if (SP_IS_PATH(*i)) {
+ path = SP_PATH(*i);
+ curve = path->getCurve();
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = paths_to_pw(curve->get_pathvector());
+ canvas_text = (SPCanvasText *) sp_canvastext_new(tmpgrp, lc->desktop, Geom::Point(0,0), "");
+ if (!show)
+ sp_canvas_item_hide(SP_CANVAS_ITEM(canvas_text));
+
+ Inkscape::Util::Unit const * unit = nullptr;
+ if (prefs->getString("/tools/lpetool/unit").compare("")) {
+ unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit"));
+ } else {
+ unit = unit_table.getUnit("px");
+ }
+
+ lengthval = Geom::length(pwd2);
+ lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit);
+ arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str());
+ sp_canvastext_set_text (canvas_text, arc_length);
+ set_pos_and_anchor(canvas_text, pwd2, 0.5, 10);
+ // TODO: must we free arc_length?
+ (*lc->measuring_items)[path] = SP_CANVAS_ITEM(canvas_text);
+ }
+ }
+}
+
+void
+lpetool_delete_measuring_items(LpeTool *lc)
+{
+ std::map<SPPath *, SPCanvasItem*>::iterator i;
+ for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) {
+ sp_canvas_item_destroy(i->second);
+ }
+ lc->measuring_items->clear();
+}
+
+void
+lpetool_update_measuring_items(LpeTool *lc)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ for ( std::map<SPPath *, SPCanvasItem*>::iterator i = lc->measuring_items->begin();
+ i != lc->measuring_items->end();
+ ++i )
+ {
+ SPPath *path = i->first;
+ SPCurve *curve = path->getCurve();
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = Geom::paths_to_pw(curve->get_pathvector());
+ Inkscape::Util::Unit const * unit = nullptr;
+ if (prefs->getString("/tools/lpetool/unit").compare("")) {
+ unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit"));
+ } else {
+ unit = unit_table.getUnit("px");
+ }
+ double lengthval = Geom::length(pwd2);
+ lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit);
+ gchar *arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str());
+ sp_canvastext_set_text (SP_CANVASTEXT(i->second), arc_length);
+ set_pos_and_anchor(SP_CANVASTEXT(i->second), pwd2, 0.5, 10);
+ // TODO: must we free arc_length?
+ }
+}
+
+void
+lpetool_show_measuring_info(LpeTool *lc, bool show)
+{
+ std::map<SPPath *, SPCanvasItem*>::iterator i;
+ for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) {
+ if (show) {
+ sp_canvas_item_show(i->second);
+ } else {
+ sp_canvas_item_hide(i->second);
+ }
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/tools/lpe-tool.h b/src/ui/tools/lpe-tool.h
new file mode 100644
index 0000000..789aaf3
--- /dev/null
+++ b/src/ui/tools/lpe-tool.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SP_LPETOOL_CONTEXT_H_SEEN
+#define SP_LPETOOL_CONTEXT_H_SEEN
+
+/*
+ * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs
+ *
+ * Authors:
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/pen-tool.h"
+
+#define SP_LPETOOL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::LpeTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_LPETOOL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::LpeTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+/* This is the list of subtools from which the toolbar of the LPETool is built automatically */
+extern const int num_subtools;
+
+struct SubtoolEntry {
+ Inkscape::LivePathEffect::EffectType type;
+ gchar const *icon_name;
+};
+
+extern SubtoolEntry lpesubtools[];
+
+enum LPEToolState {
+ LPETOOL_STATE_PEN,
+ LPETOOL_STATE_NODE
+};
+
+namespace Inkscape {
+class Selection;
+}
+
+class ShapeEditor;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class LpeTool : public PenTool {
+public:
+ LpeTool();
+ ~LpeTool() override;
+
+ ShapeEditor* shape_editor;
+ SPCanvasItem *canvas_bbox;
+ Inkscape::LivePathEffect::EffectType mode;
+
+ std::map<SPPath *, SPCanvasItem*> *measuring_items;
+
+ sigc::connection sel_changed_connection;
+ sigc::connection sel_modified_connection;
+
+ static const std::string prefsPath;
+
+ const std::string& getPrefsPath() override;
+
+protected:
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+};
+
+int lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type);
+int lpetool_item_has_construction(LpeTool *lc, SPItem *item);
+bool lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type);
+void lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type);
+void lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B);
+void lpetool_context_reset_limiting_bbox(LpeTool *lc);
+void lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection = nullptr);
+void lpetool_delete_measuring_items(LpeTool *lc);
+void lpetool_update_measuring_items(LpeTool *lc);
+void lpetool_show_measuring_info(LpeTool *lc, bool show = true);
+
+}
+}
+}
+
+#endif // SP_LPETOOL_CONTEXT_H_SEEN
+
+/*
+ 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/tools/measure-tool.cpp b/src/ui/tools/measure-tool.cpp
new file mode 100644
index 0000000..caf9e3c
--- /dev/null
+++ b/src/ui/tools/measure-tool.cpp
@@ -0,0 +1,1466 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Our nice measuring tool
+ *
+ * Authors:
+ * Felipe Correa da Silva Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2011 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <boost/none_t.hpp>
+
+#include <2geom/line.h>
+#include <2geom/path-intersection.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "path-chemistry.h"
+#include "rubberband.h"
+#include "text-editing.h"
+#include "verbs.h"
+
+#include "display/curve.h"
+#include "display/sodipodi-ctrl.h"
+#include "display/sp-canvas-util.h"
+#include "display/sp-canvas.h"
+#include "display/sp-ctrlcurve.h"
+#include "display/sp-ctrlline.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+
+#include "ui/pixmaps/cursor-measure.xpm"
+
+#include "svg/stringstream.h"
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+
+#include "ui/dialog/knot-properties.h"
+#include "ui/tools/freehand-base.h"
+#include "ui/tools/measure-tool.h"
+
+#include "util/units.h"
+
+using Inkscape::ControlManager;
+using Inkscape::CTLINE_SECONDARY;
+using Inkscape::Util::unit_table;
+using Inkscape::DocumentUndo;
+
+#define MT_KNOT_COLOR_NORMAL 0xffffff00
+#define MT_KNOT_COLOR_MOUSEOVER 0xff000000
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& MeasureTool::getPrefsPath()
+{
+ return MeasureTool::prefsPath;
+}
+
+const std::string MeasureTool::prefsPath = "/tools/measure";
+
+namespace {
+
+/**
+ * Simple class to use for removing label overlap.
+ */
+class LabelPlacement {
+public:
+
+ double lengthVal;
+ double offset;
+ Geom::Point start;
+ Geom::Point end;
+};
+
+bool SortLabelPlacement(LabelPlacement const &first, LabelPlacement const &second)
+{
+ if (first.end[Geom::Y] == second.end[Geom::Y]) {
+ return first.end[Geom::X] < second.end[Geom::X];
+ } else {
+ return first.end[Geom::Y] < second.end[Geom::Y];
+ }
+}
+
+//precision is for give the number of decimal positions
+//of the label to calculate label width
+void repositionOverlappingLabels(std::vector<LabelPlacement> &placements, SPDesktop *desktop, Geom::Point const &normal, double fontsize, int precision)
+{
+ std::sort(placements.begin(), placements.end(), SortLabelPlacement);
+
+ double border = 3;
+ Geom::Rect box;
+ {
+ Geom::Point tmp(fontsize * (6 + precision) + (border * 2), fontsize + (border * 2));
+ tmp = desktop->w2d(tmp);
+ box = Geom::Rect(-tmp[Geom::X] / 2, -tmp[Geom::Y] / 2, tmp[Geom::X] / 2, tmp[Geom::Y] / 2);
+ }
+
+ // Using index since vector may be re-ordered as we go.
+ // Starting at one, since the first item can't overlap itself
+ for (size_t i = 1; i < placements.size(); i++) {
+ LabelPlacement &place = placements[i];
+
+ bool changed = false;
+ do {
+ Geom::Rect current(box + place.end);
+
+ changed = false;
+ bool overlaps = false;
+ for (size_t j = i; (j > 0) && !overlaps; --j) {
+ LabelPlacement &otherPlace = placements[j - 1];
+ Geom::Rect target(box + otherPlace.end);
+ if (current.intersects(target)) {
+ overlaps = true;
+ }
+ }
+ if (overlaps) {
+ place.offset += (fontsize + border);
+ place.end = place.start - desktop->w2d(normal * place.offset);
+ changed = true;
+ }
+ } while (changed);
+
+ std::sort(placements.begin(), placements.begin() + i + 1, SortLabelPlacement);
+ }
+}
+
+/**
+ * Calculates where to place the anchor for the display text and arc.
+ *
+ * @param desktop the desktop that is being used.
+ * @param angle the angle to be displaying.
+ * @param baseAngle the angle of the initial baseline.
+ * @param startPoint the point that is the vertex of the selected angle.
+ * @param endPoint the point that is the end the user is manipulating for measurement.
+ * @param fontsize the size to display the text label at.
+ */
+Geom::Point calcAngleDisplayAnchor(SPDesktop *desktop, double angle, double baseAngle,
+ Geom::Point const &startPoint, Geom::Point const &endPoint,
+ double fontsize)
+{
+ // Time for the trick work of figuring out where things should go, and how.
+ double lengthVal = (endPoint - startPoint).length();
+ double effective = baseAngle + (angle / 2);
+ Geom::Point where(lengthVal, 0);
+ where *= Geom::Affine(Geom::Rotate(effective)) * Geom::Affine(Geom::Translate(startPoint));
+
+ // When the angle is tight, the label would end up under the cursor and/or lines. Bump it
+ double scaledFontsize = std::abs(fontsize * desktop->w2d(Geom::Point(0, 1.0))[Geom::Y]);
+ if (std::abs((where - endPoint).length()) < scaledFontsize) {
+ where[Geom::Y] += scaledFontsize * 2;
+ }
+
+ // We now have the ideal position, but need to see if it will fit/work.
+
+ Geom::Rect visibleArea = desktop->get_display_area();
+ // Bring it in to "title safe" for the anchor point
+ Geom::Point textBox = desktop->w2d(Geom::Point(fontsize * 3, fontsize / 2));
+ textBox[Geom::Y] = std::abs(textBox[Geom::Y]);
+
+ visibleArea = Geom::Rect(visibleArea.min()[Geom::X] + textBox[Geom::X],
+ visibleArea.min()[Geom::Y] + textBox[Geom::Y],
+ visibleArea.max()[Geom::X] - textBox[Geom::X],
+ visibleArea.max()[Geom::Y] - textBox[Geom::Y]);
+
+ where[Geom::X] = std::min(where[Geom::X], visibleArea.max()[Geom::X]);
+ where[Geom::X] = std::max(where[Geom::X], visibleArea.min()[Geom::X]);
+ where[Geom::Y] = std::min(where[Geom::Y], visibleArea.max()[Geom::Y]);
+ where[Geom::Y] = std::max(where[Geom::Y], visibleArea.min()[Geom::Y]);
+
+ return where;
+}
+
+/**
+ * Create a measure item in current document.
+ *
+ * @param pathv the path to create.
+ * @param markers if the path results get markers.
+ * @param color of the stroke.
+ * @param measure_repr container element.
+ */
+void setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop) {
+ return;
+ }
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *repr;
+ repr = xml_doc->createElement("svg:path");
+ gchar *str = sp_svg_write_path(pathv);
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ Geom::Coord strokewidth = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse().expansionX();
+ std::stringstream stroke_width;
+ stroke_width.imbue(std::locale::classic());
+ if(measure_repr) {
+ stroke_width << strokewidth / desktop->current_zoom();
+ } else {
+ stroke_width << strokewidth;
+ }
+ sp_repr_css_set_property (css, "stroke-width", stroke_width.str().c_str());
+ sp_repr_css_set_property (css, "fill", "none");
+ if(color) {
+ gchar color_line[64];
+ sp_svg_write_color (color_line, sizeof(color_line), color);
+ sp_repr_css_set_property (css, "stroke", color_line);
+ } else {
+ sp_repr_css_set_property (css, "stroke", "#ff0000");
+ }
+ char const * stroke_linecap = is_curve ? "butt" : "square";
+ sp_repr_css_set_property (css, "stroke-linecap", stroke_linecap);
+ sp_repr_css_set_property (css, "stroke-linejoin", "miter");
+ sp_repr_css_set_property (css, "stroke-miterlimit", "4");
+ sp_repr_css_set_property (css, "stroke-dasharray", "none");
+ if(measure_repr) {
+ sp_repr_css_set_property (css, "stroke-opacity", "0.5");
+ } else {
+ sp_repr_css_set_property (css, "stroke-opacity", "1");
+ }
+ if(markers) {
+ sp_repr_css_set_property (css, "marker-start", "url(#Arrow2Sstart)");
+ sp_repr_css_set_property (css, "marker-end", "url(#Arrow2Send)");
+ }
+ Glib::ustring css_str;
+ sp_repr_css_write_string(css,css_str);
+ repr->setAttribute("style", css_str);
+ sp_repr_css_attr_unref (css);
+ g_assert( str != nullptr );
+ repr->setAttribute("d", str);
+ g_free(str);
+ if(measure_repr) {
+ measure_repr->addChild(repr, nullptr);
+ Inkscape::GC::release(repr);
+ } else {
+ SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ item->updateRepr();
+ desktop->getSelection()->clear();
+ desktop->getSelection()->add(item);
+ }
+}
+
+/**
+ * Given an angle, the arc center and edge point, draw an arc segment centered around that edge point.
+ *
+ * @param desktop the desktop that is being used.
+ * @param center the center point for the arc.
+ * @param end the point that ends at the edge of the arc segment.
+ * @param anchor the anchor point for displaying the text label.
+ * @param angle the angle of the arc segment to draw.
+ * @param measure_rpr the container of the curve if converted to items.
+ */
+void createAngleDisplayCurve(SPDesktop *desktop, Geom::Point const &center, Geom::Point const &end, Geom::Point const &anchor, double angle, bool to_phantom, std::vector<SPCanvasItem *> &measure_phantom_items , std::vector<SPCanvasItem *> &measure_tmp_items , Inkscape::XML::Node *measure_repr = nullptr)
+{
+ // Given that we have a point on the arc's edge and the angle of the arc, we need to get the two endpoints.
+
+ double textLen = std::abs((anchor - center).length());
+ double sideLen = std::abs((end - center).length());
+ if (sideLen > 0.0) {
+ double factor = std::min(1.0, textLen / sideLen);
+
+ // arc start
+ Geom::Point p1 = end * (Geom::Affine(Geom::Translate(-center))
+ * Geom::Affine(Geom::Scale(factor))
+ * Geom::Affine(Geom::Translate(center)));
+
+ // arc end
+ Geom::Point p4 = p1 * (Geom::Affine(Geom::Translate(-center))
+ * Geom::Affine(Geom::Rotate(-angle))
+ * Geom::Affine(Geom::Translate(center)));
+
+ // from Riskus
+ double xc = center[Geom::X];
+ double yc = center[Geom::Y];
+ double ax = p1[Geom::X] - xc;
+ double ay = p1[Geom::Y] - yc;
+ double bx = p4[Geom::X] - xc;
+ double by = p4[Geom::Y] - yc;
+ double q1 = (ax * ax) + (ay * ay);
+ double q2 = q1 + (ax * bx) + (ay * by);
+
+ double k2 = (4.0 / 3.0) * (std::sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx));
+
+ Geom::Point p2(xc + ax - (k2 * ay),
+ yc + ay + (k2 * ax));
+ Geom::Point p3(xc + bx + (k2 * by),
+ yc + by - (k2 * bx));
+ SPCtrlCurve *curve = ControlManager::getManager().createControlCurve(desktop->getTempGroup(), p1, p2, p3, p4, CTLINE_SECONDARY);
+ if(to_phantom){
+ curve->rgba = 0x8888887f;
+ measure_phantom_items.push_back(SP_CANVAS_ITEM(curve));
+ } else {
+ measure_tmp_items.push_back(SP_CANVAS_ITEM(curve));
+ }
+ sp_canvas_item_move_to_z(SP_CANVAS_ITEM(curve), 0);
+ sp_canvas_item_show(SP_CANVAS_ITEM(curve));
+ if(measure_repr) {
+ Geom::PathVector pathv;
+ Geom::Path path;
+ path.start(desktop->doc2dt(p1));
+ path.appendNew<Geom::CubicBezier>(desktop->doc2dt(p2),desktop->doc2dt(p3),desktop->doc2dt(p4));
+ pathv.push_back(path);
+ pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ if(!pathv.empty()) {
+ setMeasureItem(pathv, true, false, 0xff00007f, measure_repr);
+ }
+ }
+ }
+}
+
+} // namespace
+
+boost::optional<Geom::Point> explicit_base_tmp = boost::none;
+
+MeasureTool::MeasureTool()
+ : ToolBase(cursor_measure_xpm)
+ , grabbed(nullptr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ start_p = readMeasurePoint(true);
+ end_p = readMeasurePoint(false);
+ dimension_offset = 35;
+ last_pos = Geom::Point(0,0);
+ // create the knots
+ this->knot_start = new SPKnot(desktop, _("Measure start, <b>Shift+Click</b> for position dialog"));
+ this->knot_start->setMode(SP_KNOT_MODE_XOR);
+ this->knot_start->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER);
+ this->knot_start->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f);
+ this->knot_start->setShape(SP_KNOT_SHAPE_CIRCLE);
+ this->knot_start->updateCtrl();
+ this->knot_end = new SPKnot(desktop, _("Measure end, <b>Shift+Click</b> for position dialog"));
+ this->knot_end->setMode(SP_KNOT_MODE_XOR);
+ this->knot_end->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER);
+ this->knot_end->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f);
+ this->knot_end->setShape(SP_KNOT_SHAPE_CIRCLE);
+ this->knot_end->updateCtrl();
+ Geom::Rect display_area = desktop->get_display_area();
+ if(display_area.interiorContains(start_p) && display_area.interiorContains(end_p) && end_p != Geom::Point()) {
+ this->knot_start->moveto(start_p);
+ this->knot_start->show();
+ this->knot_end->moveto(end_p);
+ this->knot_end->show();
+ showCanvasItems();
+ } else {
+ start_p = Geom::Point(0,0);
+ end_p = Geom::Point(0,0);
+ writeMeasurePoint(start_p, true);
+ writeMeasurePoint(end_p, false);
+ }
+ this->_knot_start_moved_connection = this->knot_start->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotStartMovedHandler));
+ this->_knot_start_click_connection = this->knot_start->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler));
+ this->_knot_start_ungrabbed_connection = this->knot_start->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler));
+ this->_knot_end_moved_connection = this->knot_end->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotEndMovedHandler));
+ this->_knot_end_click_connection = this->knot_end->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler));
+ this->_knot_end_ungrabbed_connection = this->knot_end->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler));
+
+}
+
+MeasureTool::~MeasureTool()
+{
+ this->_knot_start_moved_connection.disconnect();
+ this->_knot_start_ungrabbed_connection.disconnect();
+ this->_knot_end_moved_connection.disconnect();
+ this->_knot_end_ungrabbed_connection.disconnect();
+
+ /* unref should call destroy */
+ knot_unref(this->knot_start);
+ knot_unref(this->knot_end);
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ sp_canvas_item_destroy(measure_tmp_item);
+ }
+ measure_tmp_items.clear();
+ for (auto & idx : measure_item) {
+ sp_canvas_item_destroy(idx);
+ }
+ measure_item.clear();
+ for (auto & measure_phantom_item : measure_phantom_items) {
+ sp_canvas_item_destroy(measure_phantom_item);
+ }
+ measure_phantom_items.clear();
+}
+
+Geom::Point MeasureTool::readMeasurePoint(bool is_start) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+ return prefs->getPoint(measure_point, Geom::Point(Geom::infinity(),Geom::infinity()));
+}
+
+void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+ prefs->setPoint(measure_point, point);
+}
+
+//This function is used to reverse the Measure, I do it in two steps because when
+//we move the knot the start_ or the end_p are overwritten so I need the original values.
+void MeasureTool::reverseKnots()
+{
+ Geom::Point start = start_p;
+ Geom::Point end = end_p;
+ this->knot_start->moveto(end);
+ this->knot_start->show();
+ this->knot_end->moveto(start);
+ this->knot_end->show();
+ start_p = end;
+ end_p = start;
+ this->showCanvasItems();
+}
+
+void MeasureTool::knotClickHandler(SPKnot *knot, guint state)
+{
+ if (state & GDK_SHIFT_MASK) {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring const unit_name = prefs->getString("/tools/measure/unit");
+ explicit_base = explicit_base_tmp;
+ Inkscape::UI::Dialogs::KnotPropertiesDialog::showDialog(desktop, knot, unit_name);
+ }
+}
+
+void MeasureTool::knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state)
+{
+ Geom::Point point = this->knot_start->position();
+ if (state & GDK_CONTROL_MASK) {
+ spdc_endpoint_snap_rotation(this, point, end_p, state);
+ } else if (!(state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+ Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(this->knot_end->position());
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ point = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ if(start_p != point) {
+ start_p = point;
+ this->knot_start->moveto(start_p);
+ }
+ showCanvasItems();
+}
+
+void MeasureTool::knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state)
+{
+ Geom::Point point = this->knot_end->position();
+ if (state & GDK_CONTROL_MASK) {
+ spdc_endpoint_snap_rotation(this, point, start_p, state);
+ } else if (!(state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+ Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(this->knot_start->position());
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ point = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ if(end_p != point) {
+ end_p = point;
+ this->knot_end->moveto(end_p);
+ }
+ showCanvasItems();
+}
+
+void MeasureTool::knotUngrabbedHandler(SPKnot */*knot*/, unsigned int state)
+{
+ this->knot_start->moveto(start_p);
+ this->knot_end->moveto(end_p);
+ showCanvasItems();
+}
+
+
+//todo: we need this function?
+void MeasureTool::finish()
+{
+ this->enableGrDrag(false);
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ ToolBase::finish();
+}
+
+static void calculate_intersections(SPDesktop * /*desktop*/, SPItem* item, Geom::PathVector const &lineseg, SPCurve *curve, std::vector<double> &intersections)
+{
+ curve->transform(item->i2doc_affine());
+ // Find all intersections of the control-line with this shape
+ Geom::CrossingSet cs = Geom::crossings(lineseg, curve->get_pathvector());
+ Geom::delete_duplicates(cs[0]);
+
+ // Reconstruct and store the points of intersection
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show_hidden = prefs->getBool("/tools/measure/show_hidden", true);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ for (const auto & m : cs[0]) {
+ if (!show_hidden) {
+ double eps = 0.0001;
+ if ((m.ta > eps &&
+ item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta - eps))), true, nullptr)) ||
+ (m.ta + eps < 1 &&
+ item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta + eps))), true, nullptr))) {
+ intersections.push_back(m.ta);
+ }
+ } else {
+ intersections.push_back(m.ta);
+ }
+ }
+}
+
+bool MeasureTool::root_handler(GdkEvent* event)
+{
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS: {
+ this->knot_start->hide();
+ this->knot_end->hide();
+ Geom::Point const button_w(event->button.x, event->button.y);
+ explicit_base = boost::none;
+ explicit_base_tmp = boost::none;
+ last_end = boost::none;
+
+ if (event->button.button == 1 && !this->space_panning) {
+ // save drag origin
+ start_p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ within_tolerance = true;
+
+ ret = TRUE;
+ }
+
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+ snap_manager.freeSnapReturnByRef(start_p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ snap_manager.unSetup();
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK,
+ nullptr, event->button.time);
+ this->grabbed = SP_CANVAS_ITEM(desktop->acetate);
+ break;
+ }
+ case GDK_KEY_PRESS: {
+ if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) {
+ explicit_base_tmp = explicit_base;
+ explicit_base = end_p;
+ showInfoBox(last_pos, true);
+ }
+ break;
+ }
+ case GDK_KEY_RELEASE: {
+ if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) {
+ showInfoBox(last_pos, false);
+ }
+ break;
+ }
+ case GDK_MOTION_NOTIFY: {
+ if (!(event->motion.state & GDK_BUTTON1_MASK)) {
+ if(!(event->motion.state & GDK_SHIFT_MASK)) {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+
+ Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(start_p);
+
+ snap_manager.preSnap(scp);
+ snap_manager.unSetup();
+ }
+ last_pos = Geom::Point(event->motion.x, event->motion.y);
+ if (event->motion.state & GDK_CONTROL_MASK) {
+ showInfoBox(last_pos, true);
+ } else {
+ showInfoBox(last_pos, false);
+ }
+ } else {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ //Inkscape::Util::Unit const * unit = desktop->getNamedView()->getDisplayUnit();
+ for (auto & idx : measure_item) {
+ sp_canvas_item_destroy(idx);
+ }
+ measure_item.clear();
+ ret = TRUE;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ if ( within_tolerance) {
+ if ( Geom::LInfty( motion_w - start_p ) < tolerance) {
+ return FALSE; // Do not drag if we're within tolerance from origin.
+ }
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+ if(event->motion.time == 0 || !last_end || Geom::LInfty( motion_w - *last_end ) > (tolerance/4.0)) {
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+ end_p = motion_dt;
+
+ if (event->motion.state & GDK_CONTROL_MASK) {
+ spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state);
+ } else if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+ Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(start_p);
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ end_p = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ showCanvasItems();
+ last_end = motion_w ;
+ }
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE: {
+ this->knot_start->moveto(start_p);
+ this->knot_start->show();
+ if(last_end) {
+ end_p = desktop->w2d(*last_end);
+ if (event->button.state & GDK_CONTROL_MASK) {
+ spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state);
+ } else if (!(event->button.state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = desktop->namedview->snap_manager;
+ snap_manager.setup(desktop);
+ Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(start_p);
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ end_p = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ }
+ this->knot_end->moveto(end_p);
+ this->knot_end->show();
+ showCanvasItems();
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void MeasureTool::setMarkers()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *doc = desktop->getDocument();
+ SPObject *arrowStart = doc->getObjectById("Arrow2Sstart");
+ SPObject *arrowEnd = doc->getObjectById("Arrow2Send");
+ if (!arrowStart) {
+ setMarker(true);
+ }
+ if(!arrowEnd) {
+ setMarker(false);
+ }
+}
+void MeasureTool::setMarker(bool isStart)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *doc = desktop->getDocument();
+ SPDefs *defs = doc->getDefs();
+ Inkscape::XML::Node *rmarker;
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ rmarker = xml_doc->createElement("svg:marker");
+ rmarker->setAttribute("id", isStart ? "Arrow2Sstart" : "Arrow2Send");
+ rmarker->setAttribute("inkscape:isstock", "true");
+ rmarker->setAttribute("inkscape:stockid", isStart ? "Arrow2Sstart" : "Arrow2Send");
+ rmarker->setAttribute("orient", "auto");
+ rmarker->setAttribute("refX", "0.0");
+ rmarker->setAttribute("refY", "0.0");
+ rmarker->setAttribute("style", "overflow:visible;");
+ SPItem *marker = SP_ITEM(defs->appendChildRepr(rmarker));
+ Inkscape::GC::release(rmarker);
+ marker->updateRepr();
+ Inkscape::XML::Node *rpath;
+ rpath = xml_doc->createElement("svg:path");
+ rpath->setAttribute("d", "M 8.72,4.03 L -2.21,0.02 L 8.72,-4.00 C 6.97,-1.63 6.98,1.62 8.72,4.03 z");
+ rpath->setAttribute("id", isStart ? "Arrow2SstartPath" : "Arrow2SendPath");
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property (css, "stroke", "none");
+ sp_repr_css_set_property (css, "fill", "#000000");
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ Glib::ustring css_str;
+ sp_repr_css_write_string(css,css_str);
+ rpath->setAttribute("style", css_str);
+ sp_repr_css_attr_unref (css);
+ rpath->setAttribute("transform", isStart ? "scale(0.3) translate(-2.3,0)" : "scale(0.3) rotate(180) translate(-2.3,0)");
+ SPItem *path = SP_ITEM(marker->appendChildRepr(rpath));
+ Inkscape::GC::release(rpath);
+ path->updateRepr();
+}
+
+void MeasureTool::toGuides()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ SPDocument *doc = desktop->getDocument();
+ Geom::Point start = desktop->doc2dt(start_p) * desktop->doc2dt();
+ Geom::Point end = desktop->doc2dt(end_p) * desktop->doc2dt();
+ Geom::Ray ray(start,end);
+ SPNamedView *namedview = desktop->namedview;
+ if(!namedview) {
+ return;
+ }
+ setGuide(start,ray.angle(), _("Measure"));
+ if(explicit_base) {
+ explicit_base = *explicit_base * SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ ray.setPoints(start, *explicit_base);
+ if(ray.angle() != 0) {
+ setGuide(start,ray.angle(), _("Base"));
+ }
+ }
+ setGuide(start,0,"");
+ setGuide(start,Geom::rad_from_deg(90),_("Start"));
+ setGuide(end,0,_("End"));
+ setGuide(end,Geom::rad_from_deg(90),"");
+ showCanvasItems(true);
+ doc->ensureUpToDate();
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Add guides from measure tool"));
+}
+
+void MeasureTool::toPhantom()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ SPDocument *doc = desktop->getDocument();
+ for (auto & measure_phantom_item : measure_phantom_items) {
+ sp_canvas_item_destroy(measure_phantom_item);
+ }
+ measure_phantom_items.clear();
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ sp_canvas_item_destroy(measure_tmp_item);
+ }
+ measure_tmp_items.clear();
+ showCanvasItems(false, false, true);
+ doc->ensureUpToDate();
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Keep last measure on the canvas, for reference"));
+}
+
+void MeasureTool::toItem()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ SPDocument *doc = desktop->getDocument();
+ Geom::Ray ray(start_p,end_p);
+ guint32 line_color_primary = 0x0000ff7f;
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g");
+ showCanvasItems(false, true, false, rgroup);
+ setLine(start_p,end_p, false, line_color_primary, rgroup);
+ SPItem *measure_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(rgroup));
+ Inkscape::GC::release(rgroup);
+ measure_item->updateRepr();
+ doc->ensureUpToDate();
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Convert measure to items"));
+ reset();
+}
+
+void MeasureTool::toMarkDimension()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ SPDocument *doc = desktop->getDocument();
+ setMarkers();
+ Geom::Ray ray(start_p,end_p);
+ Geom::Point start = start_p + Geom::Point::polar(ray.angle(), 5);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ dimension_offset = prefs->getDouble("/tools/measure/offset", 5.0);
+ start = start + Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset);
+ Geom::Point end = end_p + Geom::Point::polar(ray.angle(), -5);
+ end = end+ Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset);
+ guint32 color = 0x000000ff;
+ setLine(start, end, true, color);
+ Glib::ustring unit_name = prefs->getString("/tools/measure/unit");
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+ int precision = prefs->getInt("/tools/measure/precision", 2);
+ std::stringstream precision_str;
+ precision_str.imbue(std::locale::classic());
+ precision_str << "%." << precision << "f %s";
+ Geom::Point middle = Geom::middle_point(start, end);
+ double totallengthval = (end_p - start_p).length();
+ totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name);
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+ gchar *totallength_str = g_strdup_printf(precision_str.str().c_str(), totallengthval * scale, unit_name.c_str());
+ double textangle = Geom::rad_from_deg(180) - ray.angle();
+ if (desktop->is_yaxisdown()) {
+ textangle = ray.angle() - Geom::rad_from_deg(180);
+ }
+ setLabelText(totallength_str, middle, fontsize, textangle, color);
+ g_free(totallength_str);
+ doc->ensureUpToDate();
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Add global measure line"));
+}
+
+void MeasureTool::setGuide(Geom::Point origin, double angle, const char *label)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ SPRoot const *root = doc->getRoot();
+ Geom::Affine affine(Geom::identity());
+ if(root) {
+ affine *= root->c2p.inverse();
+ }
+ SPNamedView *namedview = desktop->namedview;
+ if(!namedview) {
+ return;
+ }
+
+ // <sodipodi:guide> stores inverted y-axis coordinates
+ if (desktop->is_yaxisdown()) {
+ origin[Geom::Y] = doc->getHeight().value("px") - origin[Geom::Y];
+ angle *= -1.0;
+ }
+
+ origin *= affine;
+ //measure angle
+ Inkscape::XML::Node *guide;
+ guide = xml_doc->createElement("sodipodi:guide");
+ std::stringstream position;
+ position.imbue(std::locale::classic());
+ position << origin[Geom::X] << "," << origin[Geom::Y];
+ guide->setAttribute("position", position.str() );
+ guide->setAttribute("inkscape:color", "rgb(167,0,255)");
+ guide->setAttribute("inkscape:label", label);
+ Geom::Point unit_vector = Geom::rot90(origin.polar(angle));
+ std::stringstream angle_str;
+ angle_str.imbue(std::locale::classic());
+ angle_str << unit_vector[Geom::X] << "," << unit_vector[Geom::Y];
+ guide->setAttribute("orientation", angle_str.str());
+ namedview->appendChild(guide);
+ Inkscape::GC::release(guide);
+}
+
+void MeasureTool::setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, Inkscape::XML::Node *measure_repr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite()) {
+ return;
+ }
+ Geom::PathVector pathv;
+ Geom::Path path;
+ path.start(desktop->doc2dt(start_point));
+ path.appendNew<Geom::LineSegment>(desktop->doc2dt(end_point));
+ pathv.push_back(path);
+ pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ if(!pathv.empty()) {
+ setMeasureItem(pathv, false, markers, color, measure_repr);
+ }
+}
+
+void MeasureTool::setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !origin.isFinite()) {
+ return;
+ }
+ char const * svgd;
+ svgd = "m 0.707,0.707 6.586,6.586 m 0,-6.586 -6.586,6.586";
+ Geom::PathVector pathv = sp_svg_read_pathv(svgd);
+ Geom::Scale scale = Geom::Scale(desktop->current_zoom()).inverse();
+ pathv *= Geom::Translate(Geom::Point(-3.5,-3.5));
+ pathv *= scale;
+ pathv *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5));
+ pathv *= Geom::Translate(desktop->doc2dt(origin));
+ pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ if (!pathv.empty()) {
+ guint32 line_color_secondary = 0xff0000ff;
+ setMeasureItem(pathv, false, false, line_color_secondary, measure_repr);
+ }
+}
+
+void MeasureTool::setLabelText(const char *value, Geom::Point pos, double fontsize, Geom::Coord angle, guint32 background, Inkscape::XML::Node *measure_repr, CanvasTextAnchorPositionEnum text_anchor)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ /* Create <text> */
+ pos = desktop->doc2dt(pos);
+ Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text");
+ rtext->setAttribute("xml:space", "preserve");
+
+
+ /* Set style */
+ sp_desktop_apply_style_tool(desktop, rtext, "/tools/text", true);
+ if(measure_repr) {
+ sp_repr_set_svg_double(rtext, "x", 2);
+ sp_repr_set_svg_double(rtext, "y", 2);
+ } else {
+ sp_repr_set_svg_double(rtext, "x", 0);
+ sp_repr_set_svg_double(rtext, "y", 0);
+ }
+
+ /* Create <tspan> */
+ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
+ rtspan->setAttribute("sodipodi:role", "line");
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ std::stringstream font_size;
+ font_size.imbue(std::locale::classic());
+ if(measure_repr) {
+ font_size << fontsize;
+ } else {
+ font_size << fontsize << "pt";
+ }
+ sp_repr_css_set_property (css, "font-size", font_size.str().c_str());
+ sp_repr_css_set_property (css, "font-style", "normal");
+ sp_repr_css_set_property (css, "font-weight", "normal");
+ sp_repr_css_set_property (css, "line-height", "125%");
+ sp_repr_css_set_property (css, "letter-spacing", "0");
+ sp_repr_css_set_property (css, "word-spacing", "0");
+ sp_repr_css_set_property (css, "text-align", "center");
+ sp_repr_css_set_property (css, "text-anchor", "middle");
+ if(measure_repr) {
+ sp_repr_css_set_property (css, "fill", "#FFFFFF");
+ } else {
+ sp_repr_css_set_property (css, "fill", "#000000");
+ }
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ sp_repr_css_set_property (css, "stroke", "none");
+ Glib::ustring css_str;
+ sp_repr_css_write_string(css,css_str);
+ rtspan->setAttribute("style", css_str);
+ sp_repr_css_attr_unref (css);
+ rtext->addChild(rtspan, nullptr);
+ Inkscape::GC::release(rtspan);
+ /* Create TEXT */
+ Inkscape::XML::Node *rstring = xml_doc->createTextNode(value);
+ rtspan->addChild(rstring, nullptr);
+ Inkscape::GC::release(rstring);
+ SPItem *text_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(rtext));
+ Inkscape::GC::release(rtext);
+ text_item->updateRepr();
+ Geom::OptRect bbox = text_item->geometricBounds();
+ if (!measure_repr && bbox) {
+ Geom::Point center = bbox->midpoint();
+ text_item->transform *= Geom::Translate(center).inverse();
+ pos += Geom::Point::polar(angle+ Geom::rad_from_deg(90), -bbox->height());
+ }
+ if(measure_repr) {
+ /* Create <group> */
+ Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g");
+ /* Create <rect> */
+ Inkscape::XML::Node *rrect = xml_doc->createElement("svg:rect");
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar color_line[64];
+ sp_svg_write_color (color_line, sizeof(color_line), background);
+ sp_repr_css_set_property (css, "fill", color_line);
+ sp_repr_css_set_property (css, "fill-opacity", "0.5");
+ sp_repr_css_set_property (css, "stroke-width", "0");
+ Glib::ustring css_str;
+ sp_repr_css_write_string(css,css_str);
+ rrect->setAttribute("style", css_str);
+ sp_repr_css_attr_unref (css);
+ sp_repr_set_svg_double(rgroup, "x", 0);
+ sp_repr_set_svg_double(rgroup, "y", 0);
+ sp_repr_set_svg_double(rrect, "x", -bbox->width()/2.0);
+ sp_repr_set_svg_double(rrect, "y", -bbox->height());
+ sp_repr_set_svg_double(rrect, "width", bbox->width() + 6);
+ sp_repr_set_svg_double(rrect, "height", bbox->height() + 6);
+ Inkscape::XML::Node *rtextitem = text_item->getRepr();
+ text_item->deleteObject();
+ rgroup->addChild(rtextitem, nullptr);
+ Inkscape::GC::release(rtextitem);
+ rgroup->addChild(rrect, nullptr);
+ Inkscape::GC::release(rrect);
+ SPItem *text_item_box = SP_ITEM(desktop->currentLayer()->appendChildRepr(rgroup));
+ Geom::Scale scale = Geom::Scale(desktop->current_zoom()).inverse();
+ if(bbox && text_anchor == TEXT_ANCHOR_CENTER) {
+ text_item_box->transform *= Geom::Translate(bbox->midpoint() - Geom::Point(1.0,1.0)).inverse();
+ }
+ text_item_box->transform *= scale;
+ text_item_box->transform *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5));
+ text_item_box->transform *= Geom::Translate(pos);
+ text_item_box->transform *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ text_item_box->updateRepr();
+ text_item_box->doWriteTransform(text_item_box->transform, nullptr, true);
+ Inkscape::XML::Node *rlabel = text_item_box->getRepr();
+ text_item_box->deleteObject();
+ measure_repr->addChild(rlabel, nullptr);
+ Inkscape::GC::release(rlabel);
+ } else {
+ text_item->transform *= Geom::Rotate(angle);
+ text_item->transform *= Geom::Translate(pos);
+ text_item->transform *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ text_item->doWriteTransform(text_item->transform, nullptr, true);
+ }
+}
+
+void MeasureTool::reset()
+{
+ this->knot_start->hide();
+ this->knot_end->hide();
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ sp_canvas_item_destroy(measure_tmp_item);
+ }
+ measure_tmp_items.clear();
+}
+
+void MeasureTool::setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, Glib::ustring unit_name, Geom::Point position, guint32 background, CanvasTextAnchorPositionEnum text_anchor, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ std::stringstream precision_str;
+ precision_str.imbue(std::locale::classic());
+ if(is_angle){
+ precision_str << "%." << precision << "f °";
+ } else {
+ precision_str << "%." << precision << "f %s";
+ }
+ gchar *measure_str = g_strdup_printf(precision_str.str().c_str(), amount, unit_name.c_str());
+ SPCanvasText *canvas_tooltip = sp_canvastext_new(desktop->getTempGroup(),
+ desktop,
+ position,
+ measure_str);
+ sp_canvastext_set_fontsize(canvas_tooltip, fontsize);
+ canvas_tooltip->rgba = 0xffffffff;
+ canvas_tooltip->rgba_background = background;
+ canvas_tooltip->outline = false;
+ canvas_tooltip->background = true;
+ canvas_tooltip->anchor_position = text_anchor;
+ if(to_phantom){
+ canvas_tooltip->rgba_background = 0x4444447f;
+ measure_phantom_items.push_back(SP_CANVAS_ITEM(canvas_tooltip));
+ sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip));
+ } else {
+ measure_tmp_items.push_back(SP_CANVAS_ITEM(canvas_tooltip));
+ sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip));
+ }
+
+ if(to_item) {
+ setLabelText(measure_str, position, fontsize, 0, background, measure_repr);
+ }
+ g_free(measure_str);
+}
+
+void MeasureTool::setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr){
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ guint32 color = 0xff0000ff;
+ if(to_phantom){
+ color = 0x888888ff;
+ }
+ SPCanvasItem * canvasitem = sp_canvas_item_new(desktop->getTempGroup(),
+ SP_TYPE_CTRL,
+ "anchor", SP_ANCHOR_CENTER,
+ "size", 9,
+ "stroked", TRUE,
+ "stroke_color", color,
+ "mode", SP_KNOT_MODE_XOR,
+ "shape", SP_KNOT_SHAPE_CROSS,
+ NULL );
+
+ SP_CTRL(canvasitem)->moveto(position);
+ if(to_phantom){
+ measure_phantom_items.push_back(canvasitem);
+ } else {
+ measure_tmp_items.push_back(canvasitem);
+ }
+ sp_canvas_item_show(canvasitem);
+ sp_canvas_item_move_to_z(canvasitem, 0);
+
+ if(to_item) {
+ setPoint(position, measure_repr);
+ }
+}
+
+void MeasureTool::setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, Inkscape::CtrlLineType ctrl_line_type, Inkscape::XML::Node *measure_repr){
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ gint32 color = ctrl_line_type == CTLINE_PRIMARY ? 0x0000ff7f : 0xff00007f;
+ if(to_phantom){
+ color = ctrl_line_type == CTLINE_PRIMARY ? 0x4444447f : 0x8888887f;
+ }
+ SPCtrlLine *control_line = ControlManager::getManager().createControlLine(desktop->getTempGroup(),
+ start,
+ end,
+ ctrl_line_type);
+ control_line->rgba = color;
+ if(to_phantom){
+ measure_phantom_items.push_back(SP_CANVAS_ITEM(control_line));
+ } else {
+ measure_tmp_items.push_back(SP_CANVAS_ITEM(control_line));
+ }
+ sp_canvas_item_move_to_z(SP_CANVAS_ITEM(control_line), 0);
+ sp_canvas_item_show(SP_CANVAS_ITEM(control_line));
+ if(to_item) {
+ setLine(start,
+ end,
+ false,
+ color,
+ measure_repr);
+ }
+}
+
+void MeasureTool::showItemInfoText(Geom::Point pos, gchar *measure_str, double fontsize)
+{
+ SPCanvasText *canvas_tooltip = sp_canvastext_new(desktop->getTempGroup(),
+ desktop,
+ pos,
+ measure_str);
+ sp_canvastext_set_fontsize(canvas_tooltip, fontsize);
+ canvas_tooltip->rgba = 0xffffffff;
+ canvas_tooltip->outline = false;
+ canvas_tooltip->background = true;
+ canvas_tooltip->anchor_position = TEXT_ANCHOR_LEFT;
+ canvas_tooltip->rgba_background = 0x00000099;
+ measure_item.push_back(SP_CANVAS_ITEM(canvas_tooltip));
+ sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip));
+}
+
+void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::Util::Unit const * unit = desktop->getNamedView()->getDisplayUnit();
+ for (auto & idx : measure_item) {
+ sp_canvas_item_destroy(idx);
+ }
+ measure_item.clear();
+
+ SPItem *newover = desktop->getItemAtPoint(cursor, into_groups);
+ if (newover) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+ int precision = prefs->getInt("/tools/measure/precision", 2);
+ Glib::ustring unit_name = prefs->getString("/tools/measure/unit");
+ bool only_selected = prefs->getBool("/tools/measure/only_selected", false);
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+ Geom::Scale zoom = Geom::Scale(Inkscape::Util::Quantity::convert(desktop->current_zoom(), "px", unit->abbr)).inverse();
+ if(newover != over){
+ over = newover;
+ Preferences *prefs = Preferences::get();
+ int prefs_bbox = prefs->getBool("/tools/bounding_box", false);
+ SPItem::BBoxType bbox_type = !prefs_bbox ? SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX;
+ Geom::OptRect bbox = over->bounds(bbox_type);
+ if (bbox) {
+
+ item_width = Inkscape::Util::Quantity::convert((*bbox).width() * scale, unit->abbr, unit_name);
+ item_height = Inkscape::Util::Quantity::convert((*bbox).height() * scale, unit->abbr, unit_name);
+ item_x = Inkscape::Util::Quantity::convert((*bbox).left(), unit->abbr, unit_name);
+ Geom::Point y_point(0,Inkscape::Util::Quantity::convert((*bbox).bottom() * scale, unit->abbr, "px"));
+ y_point *= desktop->doc2dt();
+ item_y = Inkscape::Util::Quantity::convert(y_point[Geom::Y] * scale, "px", unit_name);
+ if (SP_IS_SHAPE(over)) {
+ Geom::PathVector shape = SP_SHAPE(over)->getCurve(true)->get_pathvector();
+ item_length = Geom::length(paths_to_pw(shape));
+ item_length = Inkscape::Util::Quantity::convert(item_length * scale, unit->abbr, unit_name);
+ }
+ }
+ }
+ gchar *measure_str = nullptr;
+ std::stringstream precision_str;
+ precision_str.imbue(std::locale::classic());
+ double origin = Inkscape::Util::Quantity::convert(14, "px", unit->abbr);
+ Geom::Point rel_position = Geom::Point(origin, origin);
+ Geom::Point pos = desktop->w2d(cursor);
+ double gap = Inkscape::Util::Quantity::convert(7 + fontsize, "px", unit->abbr);
+ if (only_selected) {
+ if (desktop->getSelection()->includes(over)) {
+ showItemInfoText(pos + (rel_position * zoom),_("Selected"),fontsize);
+ } else {
+ showItemInfoText(pos + (rel_position * zoom),_("Not selected"),fontsize);
+ }
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ }
+ if (SP_IS_SHAPE(over)) {
+ precision_str << _("Length") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_length, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ } else if (SP_IS_GROUP(over)) {
+ measure_str = _("Press 'CTRL' to measure into group");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ }
+ precision_str << "Y: %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_y, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << "X: %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_x, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << _("Height") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_height, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << _("Width") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_width, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize);
+ g_free(measure_str);
+ }
+}
+
+void MeasureTool::showCanvasItems(bool to_guides, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ writeMeasurePoint(start_p, true);
+ writeMeasurePoint(end_p, false);
+ //clear previous canvas items, we'll draw new ones
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ sp_canvas_item_destroy(measure_tmp_item);
+ }
+ measure_tmp_items.clear();
+ //TODO:Calculate the measure area for current length and origin
+ // and use canvas->requestRedraw. In the calculation need a gap for outside text
+ // maybe this remove the trash lines on measure use
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show_in_between = prefs->getBool("/tools/measure/show_in_between", true);
+ bool all_layers = prefs->getBool("/tools/measure/all_layers", true);
+ dimension_offset = 70;
+ Geom::PathVector lineseg;
+ Geom::Path p;
+ Geom::Point start_p_doc = start_p * desktop->dt2doc();
+ Geom::Point end_p_doc = end_p * desktop->dt2doc();
+ p.start(start_p_doc);
+ p.appendNew<Geom::LineSegment>(end_p_doc);
+ lineseg.push_back(p);
+
+ double angle = atan2(end_p - start_p);
+ double baseAngle = 0;
+
+ if (explicit_base) {
+ baseAngle = atan2(explicit_base.get() - start_p);
+ angle -= baseAngle;
+ }
+
+ std::vector<SPItem*> items;
+ SPDocument *doc = desktop->getDocument();
+ Geom::Rect rect(start_p_doc, end_p_doc);
+ items = doc->getItemsPartiallyInBox(desktop->dkey, rect, false, true, false, true);
+ Inkscape::LayerModel *layer_model = nullptr;
+ SPObject *current_layer = nullptr;
+ if(desktop){
+ layer_model = desktop->layers;
+ current_layer = desktop->currentLayer();
+ }
+ std::vector<double> intersection_times;
+ bool only_selected = prefs->getBool("/tools/measure/only_selected", false);
+ for (auto i : items) {
+ SPItem *item = i;
+ if (!desktop->getSelection()->includes(i) && only_selected) {
+ continue;
+ }
+ if(all_layers || (layer_model && layer_model->layerForObject(item) == current_layer)){
+ if (SP_IS_SHAPE(item)) {
+ calculate_intersections(desktop, item, lineseg, SP_SHAPE(item)->getCurve(), intersection_times);
+ } else {
+ if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
+ Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin();
+ do {
+ Inkscape::Text::Layout::iterator iter_next = iter;
+ iter_next.nextGlyph(); // iter_next is one glyph ahead from iter
+ if (iter == iter_next) {
+ break;
+ }
+
+ // get path from iter to iter_next:
+ SPCurve *curve = te_get_layout(item)->convertToCurves(iter, iter_next);
+ iter = iter_next; // shift to next glyph
+ if (!curve) {
+ continue; // error converting this glyph
+ }
+ if (curve->is_empty()) { // whitespace glyph?
+ curve->unref();
+ continue;
+ }
+
+ calculate_intersections(desktop, item, lineseg, curve, intersection_times);
+ if (iter == te_get_layout(item)->end()) {
+ break;
+ }
+ } while (true);
+ }
+ }
+ }
+ }
+ Glib::ustring unit_name = prefs->getString("/tools/measure/unit");
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+ // Normal will be used for lines and text
+ Geom::Point windowNormal = Geom::unit_vector(Geom::rot90(desktop->d2w(end_p - start_p)));
+ Geom::Point normal = desktop->w2d(windowNormal);
+
+ std::vector<Geom::Point> intersections;
+ std::sort(intersection_times.begin(), intersection_times.end());
+ for (double & intersection_time : intersection_times) {
+ intersections.push_back(lineseg[0].pointAt(intersection_time));
+ }
+
+ if(!show_in_between && intersection_times.size() > 1) {
+ Geom::Point start = lineseg[0].pointAt(intersection_times[0]);
+ Geom::Point end = lineseg[0].pointAt(intersection_times[intersection_times.size()-1]);
+ intersections.clear();
+ intersections.push_back(start);
+ intersections.push_back(end);
+ }
+ if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) {
+ intersections.insert(intersections.begin(),lineseg[0].pointAt(0));
+ intersections.push_back(lineseg[0].pointAt(1));
+ }
+ std::vector<LabelPlacement> placements;
+ for (size_t idx = 1; idx < intersections.size(); ++idx) {
+ LabelPlacement placement;
+ placement.lengthVal = (intersections[idx] - intersections[idx - 1]).length();
+ placement.lengthVal = Inkscape::Util::Quantity::convert(placement.lengthVal, "px", unit_name);
+ placement.offset = dimension_offset / 2;
+ placement.start = desktop->doc2dt( (intersections[idx - 1] + intersections[idx]) / 2 );
+ placement.end = placement.start - (normal * placement.offset);
+
+ placements.push_back(placement);
+ }
+ int precision = prefs->getInt("/tools/measure/precision", 2);
+ // Adjust positions
+ repositionOverlappingLabels(placements, desktop, windowNormal, fontsize, precision);
+ for (auto & place : placements) {
+ setMeasureCanvasText(false, precision, place.lengthVal * scale, fontsize, unit_name, place.end, 0x0000007f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr);
+ }
+ Geom::Point angleDisplayPt = calcAngleDisplayAnchor(desktop, angle, baseAngle,
+ start_p, end_p,
+ fontsize);
+ {
+ setMeasureCanvasText(true, precision, Geom::deg_from_rad(angle), fontsize, unit_name, angleDisplayPt, 0x337f337f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr);
+ }
+
+ {
+ double totallengthval = (end_p - start_p).length();
+ totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name);
+ Geom::Point origin = end_p + desktop->w2d(Geom::Point(3*fontsize, -fontsize));
+ setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x3333337f, TEXT_ANCHOR_LEFT, to_item, to_phantom, measure_repr);
+ }
+
+ if (intersections.size() > 2) {
+ double totallengthval = (intersections[intersections.size()-1] - intersections[0]).length();
+ totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name);
+ Geom::Point origin = desktop->doc2dt((intersections[0] + intersections[intersections.size()-1])/2) + normal * dimension_offset;
+ setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x33337f7f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr);
+ }
+
+ // Initial point
+ {
+ setMeasureCanvasItem(start_p, false, to_phantom, measure_repr);
+ }
+
+ // Now that text has been added, we can add lines and controls so that they go underneath
+ for (size_t idx = 0; idx < intersections.size(); ++idx) {
+ setMeasureCanvasItem(desktop->doc2dt(intersections[idx]), to_item, to_phantom, measure_repr);
+ if(to_guides) {
+ gchar *cross_number;
+ if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) {
+ cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx));
+ } else {
+ cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx + 1));
+ }
+ if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true) && idx == 0) {
+ setGuide(desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), "");
+ } else {
+ setGuide(desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), cross_number);
+ }
+ g_free(cross_number);
+ }
+ }
+ // Since adding goes to the bottom, do all lines last.
+
+ // draw main control line
+ {
+ setMeasureCanvasControlLine(start_p, end_p, false, to_phantom, CTLINE_PRIMARY, measure_repr);
+ double length = std::abs((end_p - start_p).length());
+ Geom::Point anchorEnd = start_p;
+ anchorEnd[Geom::X] += length;
+ if (explicit_base) {
+ anchorEnd *= (Geom::Affine(Geom::Translate(-start_p))
+ * Geom::Affine(Geom::Rotate(baseAngle))
+ * Geom::Affine(Geom::Translate(start_p)));
+ }
+ setMeasureCanvasControlLine(start_p, anchorEnd, to_item, to_phantom, CTLINE_SECONDARY, measure_repr);
+ createAngleDisplayCurve(desktop, start_p, end_p, angleDisplayPt, angle, to_phantom, measure_phantom_items, measure_tmp_items, measure_repr);
+ }
+
+ if (intersections.size() > 2) {
+ setMeasureCanvasControlLine(desktop->doc2dt(intersections[0]) + normal * dimension_offset, desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr);
+
+ setMeasureCanvasControlLine(desktop->doc2dt(intersections[0]), desktop->doc2dt(intersections[0]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr);
+
+ setMeasureCanvasControlLine(desktop->doc2dt(intersections[intersections.size() - 1]), desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr);
+ }
+
+ // call-out lines
+ for (auto & place : placements) {
+ setMeasureCanvasControlLine(place.start, place.end, to_item, to_phantom, CTLINE_SECONDARY, measure_repr);
+ }
+
+ {
+ for (size_t idx = 1; idx < intersections.size(); ++idx) {
+ Geom::Point measure_text_pos = (intersections[idx - 1] + intersections[idx]) / 2;
+ setMeasureCanvasControlLine(desktop->doc2dt(measure_text_pos), desktop->doc2dt(measure_text_pos) - (normal * dimension_offset / 2), to_item, to_phantom, CTLINE_SECONDARY, measure_repr);
+ }
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/tools/measure-tool.h b/src/ui/tools/measure-tool.h
new file mode 100644
index 0000000..a6ca39e
--- /dev/null
+++ b/src/ui/tools/measure-tool.h
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MEASURING_CONTEXT_H
+#define SEEN_SP_MEASURING_CONTEXT_H
+
+/*
+ * Our fine measuring tool
+ *
+ * Authors:
+ * Felipe Correa da Silva Sanches <juca@members.fsf.org>
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ * Copyright (C) 2011 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include "ui/tools/tool-base.h"
+#include <2geom/point.h>
+#include "display/canvas-text.h"
+#include "display/canvas-temporary-item.h"
+#include "ui/control-manager.h"
+#include <boost/optional.hpp>
+
+#define SP_MEASURE_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeasureTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_MEASURE_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeasureTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+class SPKnot;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class MeasureTool : public ToolBase {
+public:
+ MeasureTool();
+ ~MeasureTool() override;
+
+ static const std::string prefsPath;
+
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+ virtual void showCanvasItems(bool to_guides = false, bool to_item = false, bool to_phantom = false, Inkscape::XML::Node *measure_repr = nullptr);
+ virtual void reverseKnots();
+ virtual void toGuides();
+ virtual void toPhantom();
+ virtual void toMarkDimension();
+ virtual void toItem();
+ virtual void reset();
+ virtual void setMarkers();
+ virtual void setMarker(bool isStart);
+ const std::string& getPrefsPath() override;
+ Geom::Point readMeasurePoint(bool is_start);
+ void showInfoBox(Geom::Point cursor, bool into_groups);
+ void showItemInfoText(Geom::Point pos, gchar *measure_str, double fontsize);
+ void writeMeasurePoint(Geom::Point point, bool is_start);
+ void setGuide(Geom::Point origin, double angle, const char *label);
+ void setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr);
+ void setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, Inkscape::XML::Node *measure_repr = nullptr);
+ void setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, Glib::ustring unit_name, Geom::Point position, guint32 background, CanvasTextAnchorPositionEnum text_anchor, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr);
+ void setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr);
+ void setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, Inkscape::CtrlLineType ctrl_line_type, Inkscape::XML::Node *measure_repr);
+ void setLabelText(const char *value, Geom::Point pos, double fontsize, Geom::Coord angle, guint32 background , Inkscape::XML::Node *measure_repr = nullptr, CanvasTextAnchorPositionEnum text_anchor = TEXT_ANCHOR_CENTER );
+ void knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state);
+ void knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state);
+ void knotClickHandler(SPKnot *knot, guint state);
+ void knotUngrabbedHandler(SPKnot */*knot*/, unsigned int /*state*/);
+private:
+ SPCanvasItem* grabbed;
+ boost::optional<Geom::Point> explicit_base;
+ boost::optional<Geom::Point> last_end;
+ SPKnot *knot_start;
+ SPKnot *knot_end;
+ gint dimension_offset;
+ Geom::Point start_p;
+ Geom::Point end_p;
+ Geom::Point last_pos;
+ std::vector<SPCanvasItem *> measure_tmp_items;
+ std::vector<SPCanvasItem *> measure_phantom_items;
+ std::vector<SPCanvasItem *> measure_item;
+ double item_width;
+ double item_height;
+ double item_x;
+ double item_y;
+ double item_length;
+ SPItem *over;
+ sigc::connection _knot_start_moved_connection;
+ sigc::connection _knot_start_ungrabbed_connection;
+ sigc::connection _knot_start_click_connection;
+ sigc::connection _knot_end_moved_connection;
+ sigc::connection _knot_end_click_connection;
+ sigc::connection _knot_end_ungrabbed_connection;
+};
+
+}
+}
+}
+
+#endif // SEEN_SP_MEASURING_CONTEXT_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/tools/mesh-tool.cpp b/src/ui/tools/mesh-tool.cpp
new file mode 100644
index 0000000..e7f2444
--- /dev/null
+++ b/src/ui/tools/mesh-tool.cpp
@@ -0,0 +1,1098 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mesh drawing and editing tool
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Abhishek Sharma
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+//#define DEBUG_MESH
+
+
+// Libraries
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+// General
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-drag.h"
+#include "gradient-chemistry.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection.h"
+#include "snap.h"
+#include "verbs.h"
+
+#include "display/sp-ctrlcurve.h"
+#include "display/curve.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-namedview.h"
+#include "object/sp-text.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-gradient.xpm"
+#include "ui/pixmaps/cursor-gradient-add.xpm"
+
+#include "ui/control-manager.h"
+#include "ui/tools/mesh-tool.h"
+
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void sp_mesh_new_default(MeshTool &rc);
+
+const std::string& MeshTool::getPrefsPath() {
+ return MeshTool::prefsPath;
+}
+
+const std::string MeshTool::prefsPath = "/tools/mesh";
+
+// TODO: The gradient tool class looks like a 1:1 copy.
+
+MeshTool::MeshTool()
+ : ToolBase(cursor_gradient_xpm)
+// TODO: Why are these connections stored as pointers?
+ , selcon(nullptr)
+ , subselcon(nullptr)
+ , cursor_addnode(false)
+ , node_added(false)
+ , show_handles(true)
+ , edit_fill(true)
+ , edit_stroke(true)
+{
+ // TODO: This value is overwritten in the root handler
+ this->tolerance = 6;
+}
+
+MeshTool::~MeshTool() {
+ this->enableGrDrag(false);
+
+ this->selcon->disconnect();
+ delete this->selcon;
+
+ this->subselcon->disconnect();
+ delete this->subselcon;
+}
+
+// This must match GrPointType enum sp-gradient.h
+// We should move this to a shared header (can't simply move to gradient.h since that would require
+// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!).
+const gchar *ms_handle_descr [] = {
+ N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN
+ N_("Linear gradient <b>end</b>"),
+ N_("Linear gradient <b>mid stop</b>"),
+ N_("Radial gradient <b>center</b>"),
+ N_("Radial gradient <b>radius</b>"),
+ N_("Radial gradient <b>radius</b>"),
+ N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS
+ N_("Radial gradient <b>mid stop</b>"),
+ N_("Radial gradient <b>mid stop</b>"),
+ N_("Mesh gradient <b>corner</b>"),
+ N_("Mesh gradient <b>handle</b>"),
+ N_("Mesh gradient <b>tensor</b>")
+};
+
+void MeshTool::selection_changed(Inkscape::Selection* /*sel*/) {
+ GrDrag *drag = this->_grdrag;
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ if (selection == nullptr) {
+ return;
+ }
+
+ guint n_obj = (guint) boost::distance(selection->items());
+
+ if (!drag->isNonEmpty() || selection->isEmpty()) {
+ return;
+ }
+
+ guint n_tot = drag->numDraggers();
+ guint n_sel = drag->numSelected();
+
+ //The use of ngettext in the following code is intentional even if the English singular form would never be used
+ if (n_sel == 1) {
+ if (drag->singleSelectedDraggerNumDraggables() == 1) {
+ gchar * message = g_strconcat(
+ //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message
+ _("%s selected"),
+ //TRANSLATORS: Mind the space in front. This is part of a compound message
+ ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot),
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE,
+ message,_(ms_handle_descr[drag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj);
+ } else {
+ gchar * message =
+ g_strconcat(
+ //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count)
+ ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected",
+ "One handle merging %d stops (drag with <b>Shift</b> to separate) selected",
+ drag->singleSelectedDraggerNumDraggables()),
+ ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot),
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE,message,drag->singleSelectedDraggerNumDraggables(), n_tot, n_obj);
+ }
+ } else if (n_sel > 1) {
+ //TRANSLATORS: The plural refers to number of selected mesh handles. This is part of a compound message (part two indicates selected object count)
+ gchar * message =
+ g_strconcat(ngettext("<b>%d</b> mesh handle selected out of %d","<b>%d</b> mesh handles selected out of %d",n_sel),
+ //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message
+ ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL);
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj);
+ } else if (n_sel == 0) {
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE,
+ //TRANSLATORS: The plural refers to number of selected objects
+ ngettext("<b>No</b> mesh handles selected out of %d on %d selected object",
+ "<b>No</b> mesh handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj);
+ }
+
+ // FIXME
+ // We need to update mesh gradient handles.
+ // Get gradient this drag belongs too..
+}
+
+void MeshTool::setup() {
+ ToolBase::setup();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/mesh/selcue", true)) {
+ this->enableSelectionCue();
+ }
+
+ this->enableGrDrag();
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ this->selcon = new sigc::connection(selection->connectChanged(
+ sigc::mem_fun(this, &MeshTool::selection_changed)
+ ));
+
+ this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged(
+ sigc::hide(sigc::bind(
+ sigc::mem_fun(*this, &MeshTool::selection_changed),
+ (Inkscape::Selection*)nullptr)
+ )
+ ));
+
+ sp_event_context_read(this, "show_handles");
+ sp_event_context_read(this, "edit_fill");
+ sp_event_context_read(this, "edit_stroke");
+
+ this->selection_changed(selection);
+
+}
+
+void MeshTool::set(const Inkscape::Preferences::Entry& value) {
+ Glib::ustring entry_name = value.getEntryName();
+ if (entry_name == "show_handles") {
+ this->show_handles = value.getBool(true);
+ } else if (entry_name == "edit_fill") {
+ this->edit_fill = value.getBool(true);
+ } else if (entry_name == "edit_stroke") {
+ this->edit_stroke = value.getBool(true);
+ } else {
+ ToolBase::set(value);
+ }
+}
+
+void
+sp_mesh_context_select_next (ToolBase *event_context)
+{
+ GrDrag *drag = event_context->_grdrag;
+ g_assert (drag);
+
+ GrDragger *d = drag->select_next();
+
+ event_context->desktop->scroll_to_point(d->point, 1.0);
+}
+
+void
+sp_mesh_context_select_prev (ToolBase *event_context)
+{
+ GrDrag *drag = event_context->_grdrag;
+ g_assert (drag);
+
+ GrDragger *d = drag->select_prev();
+
+ event_context->desktop->scroll_to_point(d->point, 1.0);
+}
+
+/**
+Returns vector of control lines mouse is over. Returns only first if 'first' is true.
+*/
+static std::vector<SPCtrlCurve *>
+sp_mesh_context_over_line (MeshTool *rc, Geom::Point event_p, bool first = true)
+{
+ SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop;
+
+ //Translate mouse point into proper coord system
+ rc->mousepoint_doc = desktop->w2d(event_p);
+
+ double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance;
+
+ GrDrag *drag = rc->_grdrag;
+
+ std::vector<SPCtrlCurve *> selected;
+
+ for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end(); ++l) {
+ if (!SP_IS_CTRLCURVE(*l)) continue;
+
+ SPCtrlCurve *curve = SP_CTRLCURVE(*l);
+ Geom::BezierCurveN<3> b( curve->p0, curve->p1, curve->p2, curve->p3 );
+ Geom::Coord coord = b.nearestTime( rc->mousepoint_doc ); // Coord == double
+ Geom::Point nearest = b( coord );
+
+ double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom();
+ if (dist_screen < tolerance) {
+ selected.push_back(curve);
+ if (first) {
+ break;
+ }
+ }
+ }
+ return selected;
+}
+
+
+/**
+Split row/column near the mouse point.
+*/
+static void sp_mesh_context_split_near_point(MeshTool *rc, SPItem *item, Geom::Point mouse_p, guint32 /*etime*/)
+{
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_split_near_point: entrance: " << mouse_p << std::endl;
+#endif
+
+ // item is the selected item. mouse_p the location in doc coordinates of where to add the stop
+
+ ToolBase *ec = SP_EVENT_CONTEXT(rc);
+ SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop;
+
+ double tolerance = (double) ec->tolerance;
+
+ ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom());
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH,
+ _("Split mesh row/column"));
+
+ ec->get_drag()->updateDraggers();
+}
+
+/**
+Wrapper for various mesh operations that require a list of selected corner nodes.
+ */
+void
+sp_mesh_context_corner_operation (MeshTool *rc, MeshCornerOperation operation )
+{
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_corner_operation: entrance: " << operation << std::endl;
+#endif
+
+ SPDocument *doc = nullptr;
+ GrDrag *drag = rc->_grdrag;
+
+ std::map<SPMeshGradient*, std::vector<guint> > points;
+ std::map<SPMeshGradient*, SPItem*> items;
+ std::map<SPMeshGradient*, Inkscape::PaintTarget> fill_or_stroke;
+
+ // Get list of selected draggers for each mesh.
+ // For all selected draggers (a dragger may include draggerables from different meshes).
+ for (auto dragger : drag->selected) {
+ // For all draggables of dragger (a draggable corresponds to a unique mesh).
+ for (auto d : dragger->draggables) {
+ // Only mesh corners
+ if( d->point_type != POINT_MG_CORNER ) continue;
+
+ // Find the gradient
+ SPMeshGradient *gradient = SP_MESHGRADIENT( getGradient (d->item, d->fill_or_stroke) );
+
+ // Collect points together for same gradient
+ points[gradient].push_back( d->point_i );
+ items[gradient] = d->item;
+ fill_or_stroke[gradient] = d->fill_or_stroke ? Inkscape::FOR_FILL: Inkscape::FOR_STROKE;
+ }
+ }
+
+ // Loop over meshes.
+ for( std::map<SPMeshGradient*, std::vector<guint> >::const_iterator iter = points.begin(); iter != points.end(); ++iter) {
+ SPMeshGradient *mg = SP_MESHGRADIENT( iter->first );
+ if( iter->second.size() > 0 ) {
+ guint noperation = 0;
+ switch (operation) {
+
+ case MG_CORNER_SIDE_TOGGLE:
+ // std::cout << "SIDE_TOGGLE" << std::endl;
+ noperation += mg->array.side_toggle( iter->second );
+ break;
+
+ case MG_CORNER_SIDE_ARC:
+ // std::cout << "SIDE_ARC" << std::endl;
+ noperation += mg->array.side_arc( iter->second );
+ break;
+
+ case MG_CORNER_TENSOR_TOGGLE:
+ // std::cout << "TENSOR_TOGGLE" << std::endl;
+ noperation += mg->array.tensor_toggle( iter->second );
+ break;
+
+ case MG_CORNER_COLOR_SMOOTH:
+ // std::cout << "COLOR_SMOOTH" << std::endl;
+ noperation += mg->array.color_smooth( iter->second );
+ break;
+
+ case MG_CORNER_COLOR_PICK:
+ // std::cout << "COLOR_PICK" << std::endl;
+ noperation += mg->array.color_pick( iter->second, items[iter->first] );
+ break;
+
+ case MG_CORNER_INSERT:
+ // std::cout << "INSERT" << std::endl;
+ noperation += mg->array.insert( iter->second );
+ break;
+
+ default:
+ std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl;
+ }
+
+ if( noperation > 0 ) {
+ mg->array.write( mg );
+ mg->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ doc = mg->document;
+
+ switch (operation) {
+
+ case MG_CORNER_SIDE_TOGGLE:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh path type."));
+ drag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_SIDE_ARC:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Approximated arc for mesh side."));
+ drag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_TENSOR_TOGGLE:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh tensors."));
+ drag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_COLOR_SMOOTH:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Smoothed mesh corner color."));
+ drag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_COLOR_PICK:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Picked mesh corner color."));
+ drag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_INSERT:
+ DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Inserted new row or column."));
+ break;
+
+ default:
+ std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl;
+ }
+ }
+ }
+ }
+
+ // Not needed. Update is done via gr_drag_sel_modified().
+ // drag->updateDraggers();
+
+}
+
+
+/**
+ * Scale mesh to just fit into bbox of selected items.
+ */
+void
+sp_mesh_context_fit_mesh_in_bbox (MeshTool *rc)
+{
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_fit_mesh_in_bbox: entrance: Entrance"<< std::endl;
+#endif
+
+ SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (selection == nullptr) {
+ return;
+ }
+
+ bool changed = false;
+ auto itemlist = selection->items();
+ for (auto i=itemlist.begin(); i!=itemlist.end(); ++i) {
+
+ SPItem *item = *i;
+ SPStyle *style = item->style;
+
+ if (style) {
+
+ if (style->fill.isPaintserver()) {
+ SPPaintServer *server = item->style->getFillPaintServer();
+ if ( SP_IS_MESHGRADIENT(server) ) {
+
+ Geom::OptRect item_bbox = item->geometricBounds();
+ SPMeshGradient *gradient = SP_MESHGRADIENT(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ if (style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( SP_IS_MESHGRADIENT(server) ) {
+
+ Geom::OptRect item_bbox = item->visualBounds();
+ SPMeshGradient *gradient = SP_MESHGRADIENT(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ }
+ }
+ if (changed) {
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH,
+ _("Fit mesh inside bounding box."));
+ }
+}
+
+
+/**
+Handles all keyboard and mouse input for meshs.
+Note: node/handle events are take care of elsewhere.
+*/
+bool MeshTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+
+ // Get value of fill or stroke preference
+ Inkscape::PaintTarget fill_or_stroke_pref =
+ static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke"));
+
+ GrDrag *drag = this->_grdrag;
+ g_assert (drag);
+
+ gint ret = FALSE;
+
+ auto move_handle = [&](int x_dir, int y_dir) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__SHIFT(event)) {
+ mul *= 10;
+ }
+
+ y_dir *= -desktop->yaxisdir();
+
+ if (MOD__ALT(event)) {
+ drag->selected_move_screen(mul * x_dir, mul * y_dir);
+ } else {
+ mul *= nudge;
+ drag->selected_move(mul * x_dir, mul * y_dir);
+ }
+ };
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_2BUTTON_PRESS" << std::endl;
+#endif
+
+ // Double click:
+ // If over a mesh line, divide mesh row/column
+ // If not over a line and no mesh, create new mesh for top selected object.
+
+ if ( event->button.button == 1 ) {
+
+ // Are we over a mesh line?
+ std::vector<SPCtrlCurve *> over_line =
+ sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y));
+
+ if (!over_line.empty()) {
+ // We take the first item in selection, because with doubleclick, the first click
+ // always resets selection to the single object under cursor
+ sp_mesh_context_split_near_point(this, selection->items().front(), this->mousepoint_doc, event->button.time);
+ } else {
+ // Create a new gradient with default coordinates.
+
+ // Check if object already has mesh... if it does,
+ // don't create new mesh with click-drag.
+ bool has_mesh = false;
+ if (!selection->isEmpty()) {
+ SPStyle *style = selection->items().front()->style;
+ if (style) {
+ SPPaintServer *server =
+ (fill_or_stroke_pref == Inkscape::FOR_FILL) ?
+ style->getFillPaintServer():
+ style->getStrokePaintServer();
+ if (server && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+ }
+
+ if (!has_mesh) {
+ sp_mesh_new_default(*this);
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_PRESS" << std::endl;
+#endif
+
+ // Button down
+ // If mesh already exists, do rubber band selection.
+ // Else set origin for drag which will create a new gradient.
+ if ( event->button.button == 1 && !this->space_panning ) {
+
+ // Are we over a mesh line?
+ std::vector<SPCtrlCurve *> over_line =
+ sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y), false);
+
+ if (!over_line.empty()) {
+ for (auto it : over_line) {
+ SPItem *item = it->item;
+ Inkscape::PaintTarget fill_or_stroke =
+ it->is_fill ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ GrDragger* dragger0 = drag->getDraggerFor(item, POINT_MG_CORNER, it->corner0, fill_or_stroke);
+ GrDragger* dragger1 = drag->getDraggerFor(item, POINT_MG_CORNER, it->corner1, fill_or_stroke);
+ bool add = (event->button.state & GDK_SHIFT_MASK);
+ bool toggle = (event->button.state & GDK_CONTROL_MASK);
+ if ( !add && !toggle ) {
+ drag->deselectAll();
+ }
+ drag->setSelected( dragger0, true, !toggle );
+ drag->setSelected( dragger1, true, !toggle );
+ }
+ ret = true;
+ break; // To avoid putting the following code in an else block.
+ }
+
+ Geom::Point button_w(event->button.x, event->button.y);
+
+ // save drag origin
+ this->xp = (gint) button_w[Geom::X];
+ this->yp = (gint) button_w[Geom::Y];
+ this->within_tolerance = true;
+
+ dragging = true;
+
+ Geom::Point button_dt = desktop->w2d(button_w);
+ // Check if object already has mesh... if it does,
+ // don't create new mesh with click-drag.
+ bool has_mesh = false;
+ if (!selection->isEmpty()) {
+ SPStyle *style = selection->items().front()->style;
+ if (style) {
+ SPPaintServer *server =
+ (fill_or_stroke_pref == Inkscape::FOR_FILL) ?
+ style->getFillPaintServer():
+ style->getStrokePaintServer();
+ if (server && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+ }
+
+ if (has_mesh) {
+ Inkscape::Rubberband::get(desktop)->start(desktop, button_dt);
+ }
+
+ // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to
+ // enable Ctrl+doubleclick of exactly the selected item(s)
+ if (!(event->button.state & GDK_CONTROL_MASK)) {
+ this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+ }
+
+ if (!selection->isEmpty()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ }
+
+ this->origin = button_dt;
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ // Mouse move
+ if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning ) {
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_MOTION_NOTIFY: Dragging" << std::endl;
+#endif
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point const motion_dt = this->desktop->w2d(motion_w);
+
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->move(motion_dt);
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them"));
+ } else {
+ // Do nothing. For a linear/radial gradient we follow the drag, updating the
+ // gradient as the end node is dragged. For a mesh gradient, the gradient is always
+ // created to fill the object when the drag ends.
+ }
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ ret = TRUE;
+ } else {
+ // Not dragging
+
+ // Do snapping
+ if (!drag->mouseOver() && !selection->isEmpty()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt = this->desktop->w2d(motion_w);
+
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE));
+ m.unSetup();
+ }
+
+ // Highlight corner node corresponding to side or tensor node
+ if( drag->mouseOver() ) {
+ // MESH FIXME: Light up corresponding corner node corresponding to node we are over.
+ // See "pathflash" in ui/tools/node-tool.cpp for ideas.
+ // Use desktop->add_temporary_canvasitem( SPCanvasItem, milliseconds );
+ }
+
+ // Change cursor shape if over line
+ std::vector<SPCtrlCurve *> over_line =
+ sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y));
+
+ if (this->cursor_addnode && over_line.empty()) {
+ this->cursor_shape = cursor_gradient_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_addnode = false;
+ } else if (!this->cursor_addnode && !over_line.empty()) {
+ this->cursor_shape = cursor_gradient_add_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_addnode = true;
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_RELEASE" << std::endl;
+#endif
+
+ this->xp = this->yp = 0;
+
+ if ( event->button.button == 1 && !this->space_panning ) {
+
+ // Check if over line
+ std::vector<SPCtrlCurve *> over_line =
+ sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y));
+
+ if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) {
+ if (!over_line.empty()) {
+ sp_mesh_context_split_near_point(this, over_line[0]->item,
+ this->mousepoint_doc, 0);
+ ret = TRUE;
+ }
+ } else {
+ dragging = false;
+
+ // unless clicked with Ctrl (to enable Ctrl+doubleclick).
+ if (event->button.state & GDK_CONTROL_MASK) {
+ ret = TRUE;
+ break;
+ }
+
+ if (!this->within_tolerance) {
+
+ // Check if object already has mesh... if it does,
+ // don't create new mesh with click-drag.
+ bool has_mesh = false;
+ if (!selection->isEmpty()) {
+ SPStyle *style = selection->items().front()->style;
+ if (style) {
+ SPPaintServer *server =
+ (fill_or_stroke_pref == Inkscape::FOR_FILL) ?
+ style->getFillPaintServer():
+ style->getStrokePaintServer();
+ if (server && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+ }
+
+ if (!has_mesh) {
+ sp_mesh_new_default(*this);
+ } else {
+
+ // we've been dragging, either create a new gradient
+ // or rubberband-select if we have rubberband
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+
+ if (r->is_started() && !this->within_tolerance) {
+ // this was a rubberband drag
+ if (r->getMode() == RUBBERBAND_MODE_RECT) {
+ Geom::OptRect const b = r->getRectangle();
+ if (!(event->button.state & GDK_SHIFT_MASK)) {
+ drag->deselectAll();
+ }
+ drag->selectRect(*b);
+ }
+ }
+ }
+
+ } else if (this->item_to_select) {
+ if (!over_line.empty()) {
+ // Clicked on an existing mesh line, don't change selection. This stops
+ // possible change in selection during a double click with overlapping objects
+ } else {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ drag->deselectAll();
+ selection->set(this->item_to_select);
+ }
+ }
+ } else {
+ if (!over_line.empty()) {
+ // Clicked on an existing mesh line, don't change selection. This stops
+ // possible change in selection during a double click with overlapping objects
+ } else {
+ // click in an empty space; do the same as Esc
+ if (!drag->selected.empty()) {
+ drag->deselectAll();
+ } else {
+ selection->clear();
+ }
+ }
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ }
+
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_KEY_PRESS" << std::endl;
+#endif
+
+ // FIXME: tip
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+
+ // sp_event_show_modifier_tip (this->defaultMessageContext(), event,
+ // _("FIXME<b>Ctrl</b>: snap mesh angle"),
+ // _("FIXME<b>Shift</b>: draw mesh around the starting point"),
+ // NULL);
+ break;
+
+ case GDK_KEY_A:
+ case GDK_KEY_a:
+ if (MOD__CTRL_ONLY(event) && drag->isNonEmpty()) {
+ drag->selectAll();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (!drag->selected.empty()) {
+ drag->deselectAll();
+ } else {
+ selection->clear();
+ }
+
+ ret = TRUE;
+ //TODO: make dragging escapable by Esc
+ break;
+
+ case GDK_KEY_Left: // move handle left
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(-1, 0);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Up: // move handle up
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(0, 1);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Right: // move handle right
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(1, 0);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Down: // move handle down
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ if (!MOD__CTRL(event)) { // not ctrl
+ move_handle(0, -1);
+ ret = TRUE;
+ }
+ break;
+
+ // Mesh Operations --------------------------------------------
+
+ case GDK_KEY_Insert:
+ case GDK_KEY_KP_Insert:
+ // with any modifiers:
+ sp_mesh_context_corner_operation ( this, MG_CORNER_INSERT );
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_i:
+ case GDK_KEY_I:
+ if (MOD__SHIFT_ONLY(event)) {
+ // Shift+I - insert corners (alternate keybinding for keyboards
+ // that don't have the Insert key)
+ sp_mesh_context_corner_operation ( this, MG_CORNER_INSERT );
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ if ( !drag->selected.empty() ) {
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_b: // Toggle mesh side between lineto and curveto.
+ case GDK_KEY_B:
+ if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_mesh_context_corner_operation ( this, MG_CORNER_SIDE_TOGGLE );
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_c: // Convert mesh side from generic Bezier to Bezier approximating arc,
+ case GDK_KEY_C: // preserving handle direction.
+ if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_mesh_context_corner_operation ( this, MG_CORNER_SIDE_ARC );
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_g: // Toggle mesh tensor points on/off
+ case GDK_KEY_G:
+ if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_mesh_context_corner_operation ( this, MG_CORNER_TENSOR_TOGGLE );
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_j: // Smooth corner color
+ case GDK_KEY_J:
+ if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_SMOOTH );
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_k: // Pick corner color
+ case GDK_KEY_K:
+ if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) {
+ sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_PICK );
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ break;
+
+ case GDK_KEY_RELEASE:
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_context_root_handler: GDK_KEY_RELEASE" << std::endl;
+#endif
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+// Creates a new mesh gradient.
+static void sp_mesh_new_default(MeshTool &rc) {
+ SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+
+ if (!selection->isEmpty()) {
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Inkscape::PaintTarget fill_or_stroke_pref =
+ static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke"));
+
+ // Ensure mesh is immediately editable.
+ // Editing both fill and stroke at same time doesn't work well so avoid.
+ if (fill_or_stroke_pref == Inkscape::FOR_FILL) {
+ prefs->setBool("/tools/mesh/edit_fill", true );
+ prefs->setBool("/tools/mesh/edit_stroke", false);
+ } else {
+ prefs->setBool("/tools/mesh/edit_fill", false);
+ prefs->setBool("/tools/mesh/edit_stroke", true );
+ }
+
+// HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ SPDefs *defs = document->getDefs();
+
+ auto items= selection->items();
+ for(auto i=items.begin();i!=items.end();++i){
+
+ //FIXME: see above
+ sp_repr_css_change_recursive((*i)->getRepr(), css, "style");
+
+ // Create mesh element
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient");
+
+ // privates are garbage-collectable
+ repr->setAttribute("inkscape:collect", "always");
+
+ // Attach to document
+ defs->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Get corresponding object
+ SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr));
+ mg->array.create(mg, *i, (fill_or_stroke_pref == Inkscape::FOR_FILL) ?
+ (*i)->geometricBounds() : (*i)->visualBounds());
+
+ bool isText = SP_IS_TEXT(*i);
+ sp_style_set_property_url(*i,
+ ((fill_or_stroke_pref == Inkscape::FOR_FILL) ? "fill":"stroke"),
+ mg, isText);
+
+ (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH, _("Create mesh"));
+
+ // status text; we do not track coords because this branch is run once, not all the time
+ // during drag
+ int n_objects = (int) boost::distance(selection->items());
+ rc.message_context->setF(Inkscape::NORMAL_MESSAGE,
+ ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle",
+ "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects),
+ n_objects);
+ } else {
+ desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient."));
+ }
+
+}
+}
+}
+}
+
+/*
+ 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/tools/mesh-tool.h b/src/ui/tools/mesh-tool.h
new file mode 100644
index 0000000..4964a02
--- /dev/null
+++ b/src/ui/tools/mesh-tool.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MESH_CONTEXT_H
+#define SEEN_SP_MESH_CONTEXT_H
+
+/*
+ * Mesh drawing and editing tool
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Jon A. Cruz <jon@joncruz.org.
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005,2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include "ui/tools/tool-base.h"
+
+#include "object/sp-mesh-array.h"
+
+#define SP_MESH_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeshTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_MESH_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeshTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class MeshTool : public ToolBase {
+public:
+ MeshTool();
+ ~MeshTool() override;
+
+ Geom::Point origin;
+
+ Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords
+
+ sigc::connection *selcon;
+ sigc::connection *subselcon;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ void selection_changed(Inkscape::Selection* sel);
+
+ bool cursor_addnode;
+ bool node_added;
+ bool show_handles;
+ bool edit_fill;
+ bool edit_stroke;
+
+
+};
+
+void sp_mesh_context_select_next(ToolBase *event_context);
+void sp_mesh_context_select_prev(ToolBase *event_context);
+void sp_mesh_context_corner_operation(MeshTool *event_context, MeshCornerOperation operation );
+void sp_mesh_context_fit_mesh_in_bbox(MeshTool *event_context);
+
+}
+}
+}
+
+#endif // SEEN_SP_MESH_CONTEXT_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/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp
new file mode 100644
index 0000000..bc77f91
--- /dev/null
+++ b/src/ui/tools/node-tool.cpp
@@ -0,0 +1,848 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * New node tool - implementation.
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iomanip>
+
+#include <glibmm/ustring.h>
+#include <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+
+
+
+#include "desktop.h"
+#include "document.h"
+#include "message-context.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+#include "display/sp-canvas-group.h"
+#include "display/sp-canvas.h"
+
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject.h"
+
+#include "include/macros.h"
+
+#include "object/sp-clippath.h"
+#include "object/sp-item-group.h"
+#include "object/sp-mask.h"
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+
+#include "ui/pixmaps/cursor-node-d.xpm"
+#include "ui/pixmaps/cursor-node.xpm"
+
+#include "ui/control-manager.h"
+#include "ui/shape-editor.h" // temporary!
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/curve-drag-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tool/selector.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/tool-base.h"
+
+
+/** @struct NodeTool
+ *
+ * Node tool event context.
+ *
+ * @par Architectural overview of the tool
+ * @par
+ * Here's a breakdown of what each object does.
+ * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating
+ * the other handle's position when dragged. Its move() method cannot violate the constraints.
+ * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments.
+ * Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow
+ * to MultiPathManipulator. Keeps a reference to its NodeList.
+ * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator
+ * to a node from the node. Keeps a reference to its SubpathList.
+ * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference
+ * to its PathManipulator.
+ * - PathManipulator: performs most of the single-path actions like reverse subpaths,
+ * delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator.
+ * - MultiPathManipulator: performs additional operations for actions that are not per-path,
+ * for example node joins and segment joins. Tracks the control transforms for PMs that edit
+ * clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future
+ * it might handle all shapes. Handles XML commit of actions that affect all paths or
+ * the node selection and removes PathManipulators that have no nodes left after e.g. node
+ * deletes.
+ * - ControlPointSelection: keeps track of node selection and a set of nodes that can potentially
+ * be selected. There can be more than one selection. Performs actions that require no
+ * knowledge about the path, only about the nodes, like dragging and transforms. It is not
+ * specific to nodes and can accommodate any control point derived from SelectableControlPoint.
+ * Transforms nodes in response to transform handle events.
+ * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim
+ * is to eventually use a common class for object and control point transforms.
+ * - SelectableControlPoint: base for any type of selectable point. It can belong to only one
+ * selection.
+ *
+ * @par Functionality that resides in weird places
+ * @par
+ *
+ * This list is probably incomplete.
+ * - Curve dragging: CurveDragPoint, controlled by PathManipulator
+ * - Single handle shortcuts: MultiPathManipulator::event(), ModifierTracker
+ * - Linear and spatial grow: Node, spatial grow routed to ControlPointSelection
+ * - Committing handle actions performed with the mouse: PathManipulator
+ * - Sculpting: ControlPointSelection
+ *
+ * @par Plans for the future
+ * @par
+ * - MultiPathManipulator should become a generic shape editor that manages all active manipulator,
+ * more or less like the old ShapeEditor.
+ * - Knotholder should be rewritten into one manipulator class per shape, using the control point
+ * classes. Interesting features like dragging rectangle sides could be added along the way.
+ * - Better handling of clip and mask editing, particularly in response to undo.
+ * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox
+ * controls, icons, event handling should be collected in one class, though each aspect
+ * of a tool might be in an separate class for better modularity. The long term goal is to allow
+ * tools to be defined in extensions or shared library plugins.
+ */
+
+using Inkscape::ControlManager;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& NodeTool::getPrefsPath() {
+ return NodeTool::prefsPath;
+}
+
+const std::string NodeTool::prefsPath = "/tools/nodes";
+
+SPCanvasGroup *create_control_group(SPDesktop *d);
+
+NodeTool::NodeTool()
+ : ToolBase(cursor_node_xpm)
+ , _selected_nodes(nullptr)
+ , _multipath(nullptr)
+ , edit_clipping_paths(false)
+ , edit_masks(false)
+ , flashed_item(nullptr)
+ , flash_tempitem(nullptr)
+ , _selector(nullptr)
+ , _path_data(nullptr)
+ , _transform_handle_group(nullptr)
+ , _last_over(nullptr)
+ , cursor_drag(false)
+ , show_handles(false)
+ , show_outline(false)
+ , live_outline(false)
+ , live_objects(false)
+ , show_path_direction(false)
+ , show_transform_handles(false)
+ , single_node_transform_handles(false)
+{
+}
+
+SPCanvasGroup *create_control_group(SPDesktop *d)
+{
+ return reinterpret_cast<SPCanvasGroup*>(sp_canvas_item_new(
+ d->getControls(), SP_TYPE_CANVAS_GROUP, nullptr));
+}
+
+void destroy_group(SPCanvasGroup *g)
+{
+ sp_canvas_item_destroy(SP_CANVAS_ITEM(g));
+}
+
+NodeTool::~NodeTool() {
+ this->enableGrDrag(false);
+
+ if (this->flash_tempitem) {
+ this->desktop->remove_temporary_canvasitem(this->flash_tempitem);
+ }
+ for (auto hp : this->_helperpath_tmpitem) {
+ this->desktop->remove_temporary_canvasitem(hp);
+ }
+ this->_selection_changed_connection.disconnect();
+ //this->_selection_modified_connection.disconnect();
+ this->_mouseover_changed_connection.disconnect();
+ this->_sizeUpdatedConn.disconnect();
+
+ delete this->_multipath;
+ delete this->_selected_nodes;
+ delete this->_selector;
+
+ Inkscape::UI::PathSharedData &data = *this->_path_data;
+ destroy_group(data.node_data.node_group);
+ destroy_group(data.node_data.handle_group);
+ destroy_group(data.node_data.handle_line_group);
+ destroy_group(data.outline_group);
+ destroy_group(data.dragpoint_group);
+ destroy_group(this->_transform_handle_group);
+ this->desktop->canvas->endForcedFullRedraws();
+}
+
+void NodeTool::setup() {
+ ToolBase::setup();
+
+ this->_path_data = new Inkscape::UI::PathSharedData();
+
+ Inkscape::UI::PathSharedData &data = *this->_path_data;
+ data.node_data.desktop = this->desktop;
+
+ // selector has to be created here, so that its hidden control point is on the bottom
+ this->_selector = new Inkscape::UI::Selector(this->desktop);
+
+ // Prepare canvas groups for controls. This guarantees correct z-order, so that
+ // for example a dragpoint won't obscure a node
+ data.outline_group = create_control_group(this->desktop);
+ data.node_data.handle_line_group = create_control_group(this->desktop);
+ data.dragpoint_group = create_control_group(this->desktop);
+ this->_transform_handle_group = create_control_group(this->desktop);
+ data.node_data.node_group = create_control_group(this->desktop);
+ data.node_data.handle_group = create_control_group(this->desktop);
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ this->_selection_changed_connection.disconnect();
+ this->_selection_changed_connection =
+ selection->connectChanged(sigc::mem_fun(this, &NodeTool::selection_changed));
+
+ this->_mouseover_changed_connection.disconnect();
+ this->_mouseover_changed_connection =
+ Inkscape::UI::ControlPoint::signal_mouseover_change.connect(sigc::mem_fun(this, &NodeTool::mouseover_changed));
+
+ this->_sizeUpdatedConn = ControlManager::getManager().connectCtrlSizeChanged(
+ sigc::mem_fun(this, &NodeTool::handleControlUiStyleChange)
+ );
+ if (this->_transform_handle_group) {
+ this->_selected_nodes = new Inkscape::UI::ControlPointSelection(this->desktop, this->_transform_handle_group);
+ }
+ data.node_data.selection = this->_selected_nodes;
+
+ this->_multipath = new Inkscape::UI::MultiPathManipulator(data, this->_selection_changed_connection);
+
+ this->_selector->signal_point.connect(sigc::mem_fun(this, &NodeTool::select_point));
+ this->_selector->signal_area.connect(sigc::mem_fun(this, &NodeTool::select_area));
+
+ this->_multipath->signal_coords_changed.connect(
+ sigc::bind(
+ sigc::mem_fun(*this->desktop, &SPDesktop::emitToolSubselectionChanged),
+ (void*)nullptr
+ )
+ );
+
+ this->_selected_nodes->signal_selection_changed.connect(
+ // Hide both signal parameters and bind the function parameter to 0
+ // sigc::signal<void, SelectableControlPoint *, bool>
+ // <=>
+ // void update_tip(GdkEvent *event)
+ sigc::hide(sigc::hide(sigc::bind(
+ sigc::mem_fun(this, &NodeTool::update_tip),
+ (GdkEvent*)nullptr
+ )))
+ );
+
+ this->cursor_drag = false;
+ this->show_transform_handles = true;
+ this->single_node_transform_handles = false;
+ this->flash_tempitem = nullptr;
+ this->flashed_item = nullptr;
+ this->_last_over = nullptr;
+
+ // read prefs before adding items to selection to prevent momentarily showing the outline
+ sp_event_context_read(this, "show_handles");
+ sp_event_context_read(this, "show_outline");
+ sp_event_context_read(this, "live_outline");
+ sp_event_context_read(this, "live_objects");
+ sp_event_context_read(this, "show_path_direction");
+ sp_event_context_read(this, "show_transform_handles");
+ sp_event_context_read(this, "single_node_transform_handles");
+ sp_event_context_read(this, "edit_clipping_paths");
+ sp_event_context_read(this, "edit_masks");
+
+ this->selection_changed(selection);
+ this->update_tip(nullptr);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/nodes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/nodes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+
+ this->desktop->emitToolSubselectionChanged(nullptr); // sets the coord entry fields to inactive
+ sp_update_helperpath();
+}
+
+// Clean selection on tool change
+void NodeTool::finish()
+{
+ this->_selected_nodes->clear();
+ ToolBase::finish();
+}
+
+// show helper paths of the applied LPE, if any
+void sp_update_helperpath() {
+ SPDesktop * desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop || !tools_isactive(desktop, TOOLS_NODES)) {
+ return;
+ }
+ Inkscape::UI::Tools::NodeTool *nt = static_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context);
+ Inkscape::Selection *selection = desktop->getSelection();
+ for (auto hp : nt->_helperpath_tmpitem) {
+ desktop->remove_temporary_canvasitem(hp);
+ }
+ nt->_helperpath_tmpitem.clear();
+ std::vector<SPItem *> vec(selection->items().begin(), selection->items().end());
+ std::vector<std::pair<Geom::PathVector, Geom::Affine>> cs;
+ for (auto item : vec) {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ Inkscape::LivePathEffect::Effect *lpe = SP_LPE_ITEM(lpeitem)->getCurrentLPE();
+ if (lpe && lpe->isVisible()/* && lpe->showOrigPath()*/) {
+ std::vector<Geom::Point> selectedNodesPositions;
+ if (nt->_selected_nodes) {
+ Inkscape::UI::ControlPointSelection *selectionNodes = nt->_selected_nodes;
+ for (auto selectionNode : *selectionNodes) {
+ Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selectionNode);
+ selectedNodesPositions.push_back(n->position());
+ }
+ }
+ lpe->setSelectedNodePoints(selectedNodesPositions);
+ lpe->setCurrentZoom(desktop->current_zoom());
+ SPCurve *c = new SPCurve();
+ SPCurve *cc = new SPCurve();
+ std::vector<Geom::PathVector> cs = lpe->getCanvasIndicators(lpeitem);
+ for (auto &p : cs) {
+ cc->set_pathvector(p);
+ c->append(cc, false);
+ cc->reset();
+ }
+ if (!c->is_empty()) {
+ SPCanvasItem *helperpath = sp_canvas_bpath_new(desktop->getTempGroup(), c, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(helperpath), 0x0000ff9A, 1.0, SP_STROKE_LINEJOIN_MITER,
+ SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(helperpath), 0, SP_WIND_RULE_NONZERO);
+ sp_canvas_item_affine_absolute(helperpath, lpeitem->i2dt_affine());
+ nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath, 0));
+ SPCanvasItem *helperpath_back = sp_canvas_bpath_new(desktop->getTempGroup(), c, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(helperpath_back), 0xFFFFFF33, 3.0, SP_STROKE_LINEJOIN_MITER,
+ SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(helperpath_back), 0, SP_WIND_RULE_NONZERO);
+ sp_canvas_item_affine_absolute(helperpath_back, lpeitem->i2dt_affine());
+ nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath_back, 0));
+ }
+ c->unref();
+ cc->unref();
+ }
+ }
+ }
+}
+
+void NodeTool::set(const Inkscape::Preferences::Entry& value) {
+ Glib::ustring entry_name = value.getEntryName();
+
+ if (entry_name == "show_handles") {
+ this->show_handles = value.getBool(true);
+ this->_multipath->showHandles(this->show_handles);
+ } else if (entry_name == "show_outline") {
+ this->show_outline = value.getBool();
+ this->_multipath->showOutline(this->show_outline);
+ } else if (entry_name == "live_outline") {
+ this->live_outline = value.getBool();
+ this->_multipath->setLiveOutline(this->live_outline);
+ } else if (entry_name == "live_objects") {
+ this->live_objects = value.getBool();
+ this->_multipath->setLiveObjects(this->live_objects);
+ } else if (entry_name == "show_path_direction") {
+ this->show_path_direction = value.getBool();
+ this->_multipath->showPathDirection(this->show_path_direction);
+ } else if (entry_name == "show_transform_handles") {
+ this->show_transform_handles = value.getBool(true);
+ this->_selected_nodes->showTransformHandles(
+ this->show_transform_handles, this->single_node_transform_handles);
+ } else if (entry_name == "single_node_transform_handles") {
+ this->single_node_transform_handles = value.getBool();
+ this->_selected_nodes->showTransformHandles(
+ this->show_transform_handles, this->single_node_transform_handles);
+ } else if (entry_name == "edit_clipping_paths") {
+ this->edit_clipping_paths = value.getBool();
+ this->selection_changed(this->desktop->selection);
+ } else if (entry_name == "edit_masks") {
+ this->edit_masks = value.getBool();
+ this->selection_changed(this->desktop->selection);
+ } else {
+ ToolBase::set(value);
+ }
+}
+
+/** Recursively collect ShapeRecords */
+static
+void gather_items(NodeTool *nt, SPItem *base, SPObject *obj, Inkscape::UI::ShapeRole role,
+ std::set<Inkscape::UI::ShapeRecord> &s)
+{
+ using namespace Inkscape::UI;
+
+ if (!obj) {
+ return;
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ if (role != SHAPE_ROLE_NORMAL && (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj))) {
+ for (auto& c: obj->children) {
+ gather_items(nt, base, &c, role, s);
+ }
+ } else if (SP_IS_ITEM(obj)) {
+ SPObject *object = obj;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ ShapeRecord r;
+ r.object = object;
+ // TODO add support for objectBoundingBox
+ r.edit_transform = base ? base->i2doc_affine() : Geom::identity();
+ r.role = role;
+
+ if (s.insert(r).second) {
+ // this item was encountered the first time
+ if (nt->edit_clipping_paths) {
+ gather_items(nt, item, item->getClipObject(), SHAPE_ROLE_CLIPPING_PATH, s);
+ }
+
+ if (nt->edit_masks) {
+ gather_items(nt, item, item->getMaskObject(), SHAPE_ROLE_MASK, s);
+ }
+ }
+ }
+}
+
+void NodeTool::selection_changed(Inkscape::Selection *sel) {
+ using namespace Inkscape::UI;
+
+ std::set<ShapeRecord> shapes;
+
+ auto items= sel->items();
+ for(auto i=items.begin();i!=items.end();++i){
+ SPItem *item = *i;
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ sp_lpe_item_update_patheffect(lpeitem, true, true);
+ }
+ if (item) {
+ gather_items(this, nullptr, item, SHAPE_ROLE_NORMAL, shapes);
+ }
+ }
+
+ // use multiple ShapeEditors for now, to allow editing many shapes at once
+ // needs to be rethought
+ for (boost::ptr_map<SPItem*, ShapeEditor>::iterator i = this->_shape_editors.begin();
+ i != this->_shape_editors.end(); )
+ {
+ ShapeRecord s;
+ s.object = dynamic_cast<SPObject *>(i->first);
+
+ if (shapes.find(s) == shapes.end()) {
+ this->_shape_editors.erase(i++);
+ } else {
+ ++i;
+ }
+ }
+
+ for (const auto & r : shapes) {
+ if ((SP_IS_SHAPE(r.object) || SP_IS_TEXT(r.object) || SP_IS_GROUP(r.object) || SP_IS_OBJECTGROUP(r.object)) &&
+ this->_shape_editors.find(SP_ITEM(r.object)) == this->_shape_editors.end()) {
+ ShapeEditor *si = new ShapeEditor(this->desktop, r.edit_transform);
+ SPItem *item = SP_ITEM(r.object);
+ si->set_item(item);
+ this->_shape_editors.insert(item, si);
+ }
+ }
+
+ std::vector<SPItem *> vec(sel->items().begin(), sel->items().end());
+ _previous_selection = _current_selection;
+ _current_selection = vec;
+ this->_multipath->setItems(shapes);
+ this->update_tip(nullptr);
+ sp_update_helperpath();
+ // This not need to be called canvas is updated on selection change on setItems
+ // this->desktop->updateNow();
+}
+
+bool NodeTool::root_handler(GdkEvent* event) {
+ /* things to handle here:
+ * 1. selection of items
+ * 2. passing events to manipulators
+ * 3. some keybindings
+ */
+ using namespace Inkscape::UI; // pull in event helpers
+
+ desktop->getCanvas()->forceFullRedrawAfterInterruptions(5, false);
+
+ Inkscape::Selection *selection = desktop->selection;
+ static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (this->_multipath->event(this, event)) {
+ return true;
+ }
+
+ if (this->_selector->event(this, event)) {
+ return true;
+ }
+
+ if (this->_selected_nodes->event(this, event)) {
+ return true;
+ }
+ switch (event->type)
+ {
+ case GDK_MOTION_NOTIFY: {
+ sp_update_helperpath();
+ SPItem *over_item = nullptr;
+ combine_motion_events(desktop->canvas, event->motion, 0);
+ over_item = sp_event_context_find_item(desktop, event_point(event->button), FALSE, TRUE);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(this->desktop->w2d(motion_w));
+
+ SnapManager &m = this->desktop->namedview->snap_manager;
+
+ // We will show a pre-snap indication for when the user adds a node through double-clicking
+ // Adding a node will only work when a path has been selected; if that's not the case then snapping is useless
+ if (!this->desktop->selection->isEmpty()) {
+ if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ m.setup(this->desktop);
+ Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.preSnap(scp, true);
+ m.unSetup();
+ }
+ }
+
+ if (over_item && over_item != this->_last_over) {
+ this->_last_over = over_item;
+ //ink_node_tool_update_tip(nt, event);
+ this->update_tip(event);
+ }
+ // create pathflash outline
+ if (prefs->getBool("/tools/nodes/pathflash_enabled")) {
+ // We want to reset flashed item to can highligh again previous one
+ if (!over_item && this->flashed_item) {
+ this->flashed_item = nullptr;
+ break;
+ }
+ if (!over_item || over_item == this->flashed_item) {
+ break;
+ }
+
+ if (!prefs->getBool("/tools/nodes/pathflash_selected") && over_item && selection->includes(over_item)) {
+ break;
+ }
+
+ if (this->flash_tempitem) {
+ desktop->remove_temporary_canvasitem(this->flash_tempitem);
+ this->flash_tempitem = nullptr;
+ this->flashed_item = nullptr;
+ }
+
+ if (!SP_IS_SHAPE(over_item)) {
+ break; // for now, handle only shapes
+ }
+
+ this->flashed_item = over_item;
+ SPCurve *c = SP_SHAPE(over_item)->getCurveForEdit();
+
+ if (!c) {
+ break; // break out when curve doesn't exist
+ }
+
+ c->transform(over_item->i2dt_affine());
+ SPCanvasItem *flash = sp_canvas_bpath_new(desktop->getTempGroup(), c, true);
+
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(flash),
+ //prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff), 1.0,
+ over_item->highlight_color(), 1.0,
+ SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(flash), 0, SP_WIND_RULE_NONZERO);
+ this->flash_tempitem = desktop->add_temporary_canvasitem(flash,
+ prefs->getInt("/tools/nodes/pathflash_timeout", 500));
+
+ c->unref();
+ }
+ } break; // do not return true, because we need to pass this event to the parent context
+ // otherwise some features cease to work
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval(&event->key))
+ {
+ case GDK_KEY_Escape: // deselect everything
+ if (this->_selected_nodes->empty()) {
+ Inkscape::SelectionHelper::selectNone(desktop);
+ } else {
+ this->_selected_nodes->clear();
+ }
+ //ink_node_tool_update_tip(nt, event);
+ this->update_tip(event);
+ return TRUE;
+
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (held_control(event->key) && held_alt(event->key)) {
+ this->_selected_nodes->selectAll();
+ // Ctrl+A is handled in selection-chemistry.cpp via verb
+ //ink_node_tool_update_tip(nt, event);
+ this->update_tip(event);
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_h:
+ case GDK_KEY_H:
+ if (held_only_control(event->key)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/nodes/show_handles", !this->show_handles);
+ return TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+ //ink_node_tool_update_tip(nt, event);
+ this->update_tip(event);
+ break;
+
+ case GDK_KEY_RELEASE:
+ //ink_node_tool_update_tip(nt, event);
+ this->update_tip(event);
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (this->_selector->doubleClicked()) {
+ // If the selector received the doubleclick event, then we're at some distance from
+ // the path; otherwise, the doubleclick event would have been received by
+ // CurveDragPoint; we will insert nodes into the path anyway but only if we can snap
+ // to the path. Otherwise the position would not be very well defined.
+ if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(this->desktop->w2d(motion_w));
+
+ SnapManager &m = this->desktop->namedview->snap_manager;
+ m.setup(this->desktop);
+ Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), true);
+ m.unSetup();
+
+ if (sp.getSnapped()) {
+ // The first click of the double click will have cleared the path selection, because
+ // we clicked aside of the path. We need to undo this on double click
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->addList(_previous_selection);
+
+ // The selection has been restored, and the signal selection_changed has been emitted,
+ // which has again forced a restore of the _mmap variable of the MultiPathManipulator (this->_multipath)
+ // Now we can insert the new nodes as if nothing has happened!
+ this->_multipath->insertNode(this->desktop->d2w(sp.getPoint()));
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ // we realy dont want to stop any node operation we want to success all even the time consume it
+
+ return ToolBase::root_handler(event);
+}
+
+void NodeTool::update_tip(GdkEvent *event) {
+ using namespace Inkscape::UI;
+ if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) {
+ unsigned new_state = state_after_event(event);
+
+ if (new_state == event->key.state) {
+ return;
+ }
+
+ if (state_held_shift(new_state)) {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE,
+ C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection, "
+ "click to toggle object selection"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE,
+ C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection"));
+ }
+
+ return;
+ }
+ }
+
+ unsigned sz = this->_selected_nodes->size();
+ unsigned total = this->_selected_nodes->allPoints().size();
+
+ if (sz != 0) {
+ // TODO: Use Glib::ustring::compose and remove the useless copy after string freeze
+ char *nodestring_temp = g_strdup_printf(
+ ngettext("<b>%u of %u</b> node selected.", "<b>%u of %u</b> nodes selected.", total),
+ sz, total);
+ Glib::ustring nodestring(nodestring_temp);
+ g_free(nodestring_temp);
+
+ if (sz == 2) {
+ // if there are only two nodes selected, display the angle
+ // of a line going through them relative to the X axis.
+ Inkscape::UI::ControlPointSelection::Set &selection_nodes = this->_selected_nodes->allPoints();
+ std::vector<Geom::Point> positions;
+ for (auto selection_node : selection_nodes) {
+ if (selection_node->selected()) {
+ Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selection_node);
+ positions.push_back(n->position());
+ }
+ }
+ g_assert(positions.size() == 2);
+ const double angle = Geom::deg_from_rad(Geom::Line(positions[0], positions[1]).angle());
+ nodestring += " ";
+ nodestring += Glib::ustring::compose(_("Angle: %1°."),
+ Glib::ustring::format(std::fixed, std::setprecision(2), angle));
+ }
+
+ if (this->_last_over) {
+ // TRANSLATORS: The %s below is where the "%u of %u nodes selected" sentence gets put
+ char *dyntip = g_strdup_printf(C_("Node tool tip",
+ "%s Drag to select nodes, click to edit only this object (more: Shift)"),
+ nodestring.c_str());
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+ g_free(dyntip);
+ } else {
+ char *dyntip = g_strdup_printf(C_("Node tool tip",
+ "%s Drag to select nodes, click clear the selection"),
+ nodestring.c_str());
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+ g_free(dyntip);
+ }
+ } else if (!this->_multipath->empty()) {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select nodes, click to edit only this object"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select nodes, click to clear the selection"));
+ }
+ } else {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select objects to edit, click to edit this object (more: Shift)"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select objects to edit"));
+ }
+ }
+}
+
+/**
+ * @param sel Area in desktop coordinates
+ */
+void NodeTool::select_area(Geom::Rect const &sel, GdkEventButton *event) {
+ using namespace Inkscape::UI;
+
+ if (this->_multipath->empty()) {
+ // if multipath is empty, select rubberbanded items rather than nodes
+ Inkscape::Selection *selection = this->desktop->selection;
+ auto sel_doc = desktop->dt2doc() * sel;
+ std::vector<SPItem*> items = this->desktop->getDocument()->getItemsInBox(this->desktop->dkey, sel_doc);
+ selection->setList(items);
+ } else {
+ if (!held_shift(*event)) {
+ this->_selected_nodes->clear();
+ }
+
+ this->_selected_nodes->selectArea(sel);
+ }
+}
+
+void NodeTool::select_point(Geom::Point const &/*sel*/, GdkEventButton *event) {
+ using namespace Inkscape::UI; // pull in event helpers
+
+ if (!event) {
+ return;
+ }
+
+ if (event->button != 1) {
+ return;
+ }
+
+ Inkscape::Selection *selection = this->desktop->selection;
+
+ SPItem *item_clicked = sp_event_context_find_item (this->desktop, event_point(*event),
+ (event->state & GDK_MOD1_MASK) && !(event->state & GDK_CONTROL_MASK), TRUE);
+
+ if (item_clicked == nullptr) { // nothing under cursor
+ // if no Shift, deselect
+ // if there are nodes selected, the first click should deselect the nodes
+ // and the second should deselect the items
+ if (!state_held_shift(event->state)) {
+ if (this->_selected_nodes->empty()) {
+ selection->clear();
+ } else {
+ this->_selected_nodes->clear();
+ }
+ }
+ } else {
+ if (held_shift(*event)) {
+ selection->toggle(item_clicked);
+ } else {
+ selection->set(item_clicked);
+ }
+ // This not need to be called canvas is updated on selection change
+ // this->desktop->updateNow();
+ }
+}
+
+void NodeTool::mouseover_changed(Inkscape::UI::ControlPoint *p) {
+ using Inkscape::UI::CurveDragPoint;
+
+ CurveDragPoint *cdp = dynamic_cast<CurveDragPoint*>(p);
+
+ if (cdp && !this->cursor_drag) {
+ this->cursor_shape = cursor_node_d_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_drag = true;
+ } else if (!cdp && this->cursor_drag) {
+ this->cursor_shape = cursor_node_xpm;
+ this->sp_event_context_update_cursor();
+ this->cursor_drag = false;
+ }
+}
+
+void NodeTool::handleControlUiStyleChange() {
+ this->_multipath->updateHandles();
+}
+
+}
+}
+}
+
+//} // anonymous 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/tools/node-tool.h b/src/ui/tools/node-tool.h
new file mode 100644
index 0000000..2fcc8d0
--- /dev/null
+++ b/src/ui/tools/node-tool.h
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New node tool with support for multiple path editing
+ */
+/* Authors:
+ * Krzysztof KosiƄski <tweenk@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_NODE_TOOL_H
+#define SEEN_UI_TOOL_NODE_TOOL_H
+
+#include <boost/ptr_container/ptr_map.hpp>
+#include <glib.h>
+#include "ui/tools/tool-base.h"
+
+// we need it to call it from Live Effect
+#include "selection.h"
+
+namespace Inkscape {
+ namespace Display {
+ class TemporaryItem;
+ }
+
+ namespace UI {
+ class MultiPathManipulator;
+ class ControlPointSelection;
+ class Selector;
+ class ControlPoint;
+
+ struct PathSharedData;
+ }
+}
+
+struct SPCanvasGroup;
+
+#define INK_NODE_TOOL(obj) (dynamic_cast<Inkscape::UI::Tools::NodeTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define INK_IS_NODE_TOOL(obj) (dynamic_cast<const Inkscape::UI::Tools::NodeTool*>((const Inkscape::UI::Tools::ToolBase*)obj))
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class NodeTool : public ToolBase {
+public:
+ NodeTool();
+ ~NodeTool() override;
+
+ Inkscape::UI::ControlPointSelection* _selected_nodes;
+ Inkscape::UI::MultiPathManipulator* _multipath;
+ std::vector<Inkscape::Display::TemporaryItem *> _helperpath_tmpitem;
+
+ bool edit_clipping_paths;
+ bool edit_masks;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+ boost::ptr_map<SPItem*, ShapeEditor> _shape_editors;
+
+private:
+ sigc::connection _selection_changed_connection;
+ sigc::connection _mouseover_changed_connection;
+ sigc::connection _sizeUpdatedConn;
+
+ SPItem *flashed_item;
+
+ Inkscape::Display::TemporaryItem *flash_tempitem;
+ Inkscape::UI::Selector* _selector;
+ Inkscape::UI::PathSharedData* _path_data;
+ SPCanvasGroup *_transform_handle_group;
+ SPItem *_last_over;
+
+ bool cursor_drag;
+ bool show_handles;
+ bool show_outline;
+ bool live_outline;
+ bool live_objects;
+ bool show_path_direction;
+ bool show_transform_handles;
+ bool single_node_transform_handles;
+
+ std::vector<SPItem*> _current_selection;
+ std::vector<SPItem*> _previous_selection;
+
+ void selection_changed(Inkscape::Selection *sel);
+
+ void select_area(Geom::Rect const &sel, GdkEventButton *event);
+ void select_point(Geom::Point const &sel, GdkEventButton *event);
+ void mouseover_changed(Inkscape::UI::ControlPoint *p);
+ void update_tip(GdkEvent *event);
+ void handleControlUiStyleChange();
+};
+ void sp_update_helperpath();
+}
+
+}
+}
+
+#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/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp
new file mode 100644
index 0000000..a6d7858
--- /dev/null
+++ b/src/ui/tools/pen-tool.cpp
@@ -0,0 +1,2104 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Pen event context implementation.
+ */
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2004 Monash University
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/curves.h>
+
+#include "context-fns.h"
+#include "desktop.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "shortcuts.h"
+#include "verbs.h"
+
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+#include "display/sodipodi-ctrl.h"
+#include "display/sp-canvas.h"
+#include "display/sp-ctrlline.h"
+
+#include "object/sp-path.h"
+
+#include "ui/pixmaps/cursor-pen.xpm"
+
+#include "ui/control-manager.h"
+#include "ui/draw-anchor.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/pen-tool.h"
+
+// we include the necessary files for BSpline & Spiro
+#include "live_effects/lpeobject.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/parameter/path.h"
+
+#define INKSCAPE_LPE_SPIRO_C
+#include "live_effects/lpe-spiro.h"
+
+#include "helper/geom-nodetype.h"
+
+// For handling un-continuous paths:
+#include "inkscape.h"
+
+#include "live_effects/spiro.h"
+
+#define INKSCAPE_LPE_BSPLINE_C
+#include "live_effects/lpe-bspline.h"
+
+
+using Inkscape::ControlManager;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static Geom::Point pen_drag_origin_w(0, 0);
+static bool pen_within_tolerance = false;
+const double HANDLE_CUBIC_GAP = 0.001;
+
+const std::string& PenTool::getPrefsPath() {
+ return PenTool::prefsPath;
+}
+
+const std::string PenTool::prefsPath = "/tools/freehand/pen";
+
+PenTool::PenTool()
+ : FreehandBase(cursor_pen_xpm)
+ , p()
+ , previous(Geom::Point(0,0))
+ , npoints(0)
+ , mode(MODE_CLICK)
+ , state(POINT)
+ , polylines_only(false)
+ , polylines_paraxial(false)
+ , paraxial_angle(Geom::Point(0,0))
+ , num_clicks(0)
+ , expecting_clicks_for_LPE(0)
+ , waiting_LPE(nullptr)
+ , waiting_item(nullptr)
+ , c0(nullptr)
+ , c1(nullptr)
+ , cl0(nullptr)
+ , cl1(nullptr)
+ , events_disabled(false)
+{
+ tablet_enabled = false;
+}
+
+PenTool::PenTool(gchar const *const *cursor_shape)
+ : FreehandBase(cursor_shape)
+ , p()
+ , previous(Geom::Point(0,0))
+ , npoints(0)
+ , mode(MODE_CLICK)
+ , state(POINT)
+ , polylines_only(false)
+ , polylines_paraxial(false)
+ , num_clicks(0)
+ , expecting_clicks_for_LPE(0)
+ , waiting_LPE(nullptr)
+ , waiting_item(nullptr)
+ , c0(nullptr)
+ , c1(nullptr)
+ , cl0(nullptr)
+ , cl1(nullptr)
+ , events_disabled(false)
+{
+}
+
+PenTool::~PenTool() {
+ if (this->c0) {
+ sp_canvas_item_destroy(this->c0);
+ this->c0 = nullptr;
+ }
+ if (this->c1) {
+ sp_canvas_item_destroy(this->c1);
+ this->c1 = nullptr;
+ }
+ if (this->cl0) {
+ sp_canvas_item_destroy(this->cl0);
+ this->cl0 = nullptr;
+ }
+ if (this->cl1) {
+ sp_canvas_item_destroy(this->cl1);
+ this->cl1 = nullptr;
+ }
+
+ if (this->waiting_item && this->expecting_clicks_for_LPE > 0) {
+ // we received too few clicks to sanely set the parameter path so we remove the LPE from the item
+ this->waiting_item->removeCurrentPathEffect(false);
+ }
+}
+
+void PenTool::setPolylineMode() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0);
+ // change the nodes to make space for bspline mode
+ this->polylines_only = (mode == 3 || mode == 4);
+ this->polylines_paraxial = (mode == 4);
+ this->spiro = (mode == 1);
+ this->bspline = (mode == 2);
+ this->_bsplineSpiroColor();
+ if (!this->green_bpaths.empty())
+ this->_redrawAll();
+}
+
+/**
+ * Callback to initialize PenTool object.
+ */
+void PenTool::setup() {
+ FreehandBase::setup();
+ ControlManager &mgr = ControlManager::getManager();
+
+ // Pen indicators
+ this->c0 = mgr.createControl(this->desktop->getControls(), Inkscape::CTRL_TYPE_ADJ_HANDLE);
+ mgr.track(this->c0);
+
+ this->c1 = mgr.createControl(this->desktop->getControls(), Inkscape::CTRL_TYPE_ADJ_HANDLE);
+ mgr.track(this->c1);
+
+ this->cl0 = mgr.createControlLine(this->desktop->getControls());
+ this->cl1 = mgr.createControlLine(this->desktop->getControls());
+
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl0);
+ sp_canvas_item_hide(this->cl1);
+
+ sp_event_context_read(this, "mode");
+
+ this->anchor_statusbar = false;
+
+ this->setPolylineMode();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/freehand/pen/selcue")) {
+ this->enableSelectionCue();
+ }
+}
+
+void PenTool::_cancel() {
+ this->num_clicks = 0;
+ this->state = PenTool::STOP;
+ this->_resetColors();
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl0);
+ sp_canvas_item_hide(this->cl1);
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+
+ this->desktop->canvas->endForcedFullRedraws();
+}
+
+/**
+ * Finalization callback.
+ */
+void PenTool::finish() {
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (this->npoints != 0) {
+ // switching context - finish path
+ this->ea = nullptr; // unset end anchor if set (otherwise crashes)
+ this->_finish(false);
+ }
+
+ FreehandBase::finish();
+}
+
+/**
+ * Callback that sets key to value in pen context.
+ */
+void PenTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring name = val.getEntryName();
+
+ if (name == "mode") {
+ if ( val.getString() == "drag" ) {
+ this->mode = MODE_DRAG;
+ } else {
+ this->mode = MODE_CLICK;
+ }
+ }
+}
+
+bool PenTool::hasWaitingLPE() {
+ // note: waiting_LPE_type is defined in SPDrawContext
+ return (this->waiting_LPE != nullptr ||
+ this->waiting_LPE_type != Inkscape::LivePathEffect::INVALID_LPE);
+}
+
+/**
+ * Snaps new node relative to the previous node.
+ */
+void PenTool::_endpointSnap(Geom::Point &p, guint const state) const {
+ // Paraxial kicks in after first line has set the angle (before then it's a free line)
+ bool poly = this->polylines_paraxial && !this->green_curve->is_unset();
+
+ if ((state & GDK_CONTROL_MASK) && !poly) { //CTRL enables angular snapping
+ if (this->npoints > 0) {
+ spdc_endpoint_snap_rotation(this, p, this->p[0], state);
+ } else {
+ boost::optional<Geom::Point> origin = boost::optional<Geom::Point>();
+ spdc_endpoint_snap_free(this, p, origin, state);
+ }
+ } else {
+ // We cannot use shift here to disable snapping because the shift-key is already used
+ // to toggle the paraxial direction; if the user wants to disable snapping (s)he will
+ // have to use the %-key, the menu, or the snap toolbar
+ if ((this->npoints > 0) && poly) {
+ // snap constrained
+ this->_setToNearestHorizVert(p, state);
+ } else {
+ // snap freely
+ boost::optional<Geom::Point> origin = this->npoints > 0 ? this->p[0] : boost::optional<Geom::Point>();
+ spdc_endpoint_snap_free(this, p, origin, state); // pass the origin, to allow for perpendicular / tangential snapping
+ }
+ }
+}
+
+/**
+ * Snaps new node's handle relative to the new node.
+ */
+void PenTool::_endpointSnapHandle(Geom::Point &p, guint const state) const {
+ g_return_if_fail(( this->npoints == 2 ||
+ this->npoints == 5 ));
+
+ if ((state & GDK_CONTROL_MASK)) { //CTRL enables angular snapping
+ spdc_endpoint_snap_rotation(this, p, this->p[this->npoints - 2], state);
+ } else {
+ if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above
+ boost::optional<Geom::Point> origin = this->p[this->npoints - 2];
+ spdc_endpoint_snap_free(this, p, origin, state);
+ }
+ }
+}
+
+bool PenTool::item_handler(SPItem* item, GdkEvent* event) {
+ bool ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = this->_handleButtonPress(event->button);
+ break;
+ case GDK_BUTTON_RELEASE:
+ ret = this->_handleButtonRelease(event->button);
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = FreehandBase::item_handler(item, event);
+ }
+
+ return ret;
+}
+
+/**
+ * Callback to handle all pen events.
+ */
+bool PenTool::root_handler(GdkEvent* event) {
+ bool ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = this->_handleButtonPress(event->button);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ ret = this->_handleMotionNotify(event->motion);
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ ret = this->_handleButtonRelease(event->button);
+ break;
+
+ case GDK_2BUTTON_PRESS:
+ ret = this->_handle2ButtonPress(event->button);
+ break;
+
+ case GDK_KEY_PRESS:
+ ret = this->_handleKeyPress(event);
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = FreehandBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+/**
+ * Handle mouse button press event.
+ */
+bool PenTool::_handleButtonPress(GdkEventButton const &bevent) {
+ if (this->events_disabled) {
+ // skip event processing if events are disabled
+ return false;
+ }
+
+ Geom::Point const event_w(bevent.x, bevent.y);
+ Geom::Point event_dt(desktop->w2d(event_w));
+ //Test whether we hit any anchor.
+ SPDrawAnchor * const anchor = spdc_test_inside(this, event_w);
+
+ //with this we avoid creating a new point over the existing one
+ if(bevent.button != 3 && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){
+ if( anchor && anchor == this->sa && this->green_curve->is_unset()){
+ //remove the following line to avoid having one node on top of another
+ _finishSegment(event_dt, bevent.state);
+ _finish(true);
+ return true;
+ }
+ return false;
+ }
+
+ bool ret = false;
+ if (bevent.button == 1 && !this->space_panning
+ // make sure this is not the last click for a waiting LPE (otherwise we want to finish the path)
+ && this->expecting_clicks_for_LPE != 1) {
+
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return true;
+ }
+
+ if (!this->grab ) {
+ // Grab mouse, so release will not pass unnoticed
+ this->grab = SP_CANVAS_ITEM(desktop->acetate);
+ sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK ),
+ nullptr, bevent.time);
+ }
+
+ pen_drag_origin_w = event_w;
+ pen_within_tolerance = true;
+
+ switch (this->mode) {
+
+ case PenTool::MODE_CLICK:
+ // In click mode we add point on release
+ switch (this->state) {
+ case PenTool::POINT:
+ case PenTool::CONTROL:
+ case PenTool::CLOSE:
+ break;
+ case PenTool::STOP:
+ // This is allowed, if we just canceled curve
+ this->state = PenTool::POINT;
+ break;
+ default:
+ break;
+ }
+ break;
+ case PenTool::MODE_DRAG:
+ switch (this->state) {
+ case PenTool::STOP:
+ // This is allowed, if we just canceled curve
+ case PenTool::POINT:
+ if (this->npoints == 0) {
+ this->_bsplineSpiroColor();
+ Geom::Point p;
+ if ((bevent.state & GDK_CONTROL_MASK) && (this->polylines_only || this->polylines_paraxial)) {
+ p = event_dt;
+ if (!(bevent.state & GDK_SHIFT_MASK)) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ }
+ spdc_create_single_dot(this, p, "/tools/freehand/pen", bevent.state);
+ ret = true;
+ break;
+ }
+
+ // TODO: Perhaps it would be nicer to rearrange the following case
+ // distinction so that the case of a waiting LPE is treated separately
+
+ // Set start anchor
+
+ this->sa = anchor;
+ if (anchor) {
+ //Put the start overwrite curve always on the same direction
+ if (anchor->start) {
+ this->sa_overwrited = this->sa->curve->create_reverse();
+ } else {
+ this->sa_overwrited = this->sa->curve->copy();
+ }
+ this->_bsplineSpiroStartAnchor(bevent.state & GDK_SHIFT_MASK);
+ }
+ if (anchor && (!this->hasWaitingLPE()|| this->bspline || this->spiro)) {
+ // Adjust point to anchor if needed; if we have a waiting LPE, we need
+ // a fresh path to be created so don't continue an existing one
+ p = anchor->dp;
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path"));
+ } else {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ Inkscape::Selection * const selection = desktop->getSelection();
+ if (!(bevent.state & GDK_SHIFT_MASK) || this->hasWaitingLPE()) {
+ // if we have a waiting LPE, we need a fresh path to be created
+ // so don't append to an existing one
+ selection->clear();
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) {
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ }
+
+ // Create green anchor
+ p = event_dt;
+ this->_endpointSnap(p, bevent.state);
+ this->green_anchor = sp_draw_anchor_new(this, this->green_curve, true, p);
+ }
+ this->_setInitialPoint(p);
+ } else {
+ // Set end anchor
+ this->ea = anchor;
+ Geom::Point p;
+ if (anchor) {
+ p = anchor->dp;
+ // we hit an anchor, will finish the curve (either with or without closing)
+ // in release handler
+ this->state = PenTool::CLOSE;
+
+ if (this->green_anchor && this->green_anchor->active) {
+ // we clicked on the current curve start, so close it even if
+ // we drag a handle away from it
+ this->green_closed = true;
+ }
+ ret = true;
+ break;
+
+ } else {
+ p = event_dt;
+ this->_endpointSnap(p, bevent.state); // Snap node only if not hitting anchor.
+ this->_setSubsequentPoint(p, true);
+ }
+ }
+ // avoid the creation of a control point so a node is created in the release event
+ this->state = (this->spiro || this->bspline || this->polylines_only) ? PenTool::POINT : PenTool::CONTROL;
+ ret = true;
+ break;
+ case PenTool::CONTROL:
+ g_warning("Button down in CONTROL state");
+ break;
+ case PenTool::CLOSE:
+ g_warning("Button down in CLOSE state");
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (this->expecting_clicks_for_LPE == 1 && this->npoints != 0) {
+ // when the last click for a waiting LPE occurs we want to finish the path
+ this->_finishSegment(event_dt, bevent.state);
+ if (this->green_closed) {
+ // finishing at the start anchor, close curve
+ this->_finish(true);
+ } else {
+ // finishing at some other anchor, finish curve but not close
+ this->_finish(false);
+ }
+
+ ret = true;
+ } else if (bevent.button == 3 && this->npoints != 0) {
+ // right click - finish path
+ this->ea = nullptr; // unset end anchor if set (otherwise crashes)
+ this->_finish(false);
+ ret = true;
+ }
+
+ if (this->expecting_clicks_for_LPE > 0) {
+ --this->expecting_clicks_for_LPE;
+ }
+
+ return ret;
+}
+
+/**
+ * Handle motion_notify event.
+ */
+bool PenTool::_handleMotionNotify(GdkEventMotion const &mevent) {
+ bool ret = false;
+
+ if (this->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) {
+ // allow scrolling
+ return false;
+ }
+
+ if (this->events_disabled) {
+ // skip motion events if pen events are disabled
+ return false;
+ }
+
+ Geom::Point const event_w(mevent.x, mevent.y);
+
+ //we take out the function the const "tolerance" because we need it later
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ if (pen_within_tolerance) {
+ if ( Geom::LInfty( event_w - pen_drag_origin_w ) < tolerance ) {
+ return false; // Do not drag if we're within tolerance from origin.
+ }
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ pen_within_tolerance = false;
+
+ // Find desktop coordinates
+ Geom::Point p = desktop->w2d(event_w);
+
+ // Test, whether we hit any anchor
+ SPDrawAnchor *anchor = spdc_test_inside(this, event_w);
+
+ switch (this->mode) {
+ case PenTool::MODE_CLICK:
+ switch (this->state) {
+ case PenTool::POINT:
+ if ( this->npoints != 0 ) {
+ // Only set point, if we are already appending
+ this->_endpointSnap(p, mevent.state);
+ this->_setSubsequentPoint(p, true);
+ ret = true;
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ case PenTool::CONTROL:
+ case PenTool::CLOSE:
+ // Placing controls is last operation in CLOSE state
+ this->_endpointSnap(p, mevent.state);
+ this->_setCtrl(p, mevent.state);
+ ret = true;
+ break;
+ case PenTool::STOP:
+ if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ case PenTool::MODE_DRAG:
+ switch (this->state) {
+ case PenTool::POINT:
+ if ( this->npoints > 0 ) {
+ // Only set point, if we are already appending
+
+ if (!anchor) { // Snap node only if not hitting anchor
+ this->_endpointSnap(p, mevent.state);
+ this->_setSubsequentPoint(p, true, mevent.state);
+ } else {
+ this->_setSubsequentPoint(anchor->dp, false, mevent.state);
+ }
+
+ if (anchor && !this->anchor_statusbar) {
+ if(!this->spiro && !this->bspline){
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path."));
+ }else{
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path. Shift+Click make a cusp node"));
+ }
+ this->anchor_statusbar = true;
+ } else if (!anchor && this->anchor_statusbar) {
+ this->message_context->clear();
+ this->anchor_statusbar = false;
+ }
+
+ ret = true;
+ } else {
+ if (anchor && !this->anchor_statusbar) {
+ if(!this->spiro && !this->bspline){
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point."));
+ }else{
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point. Shift+Click make a cusp node"));
+ }
+ this->anchor_statusbar = true;
+ } else if (!anchor && this->anchor_statusbar) {
+ this->message_context->clear();
+ this->anchor_statusbar = false;
+
+ }
+ if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ }
+ break;
+ case PenTool::CONTROL:
+ case PenTool::CLOSE:
+ // Placing controls is last operation in CLOSE state
+
+ // snap the handle
+
+ this->_endpointSnapHandle(p, mevent.state);
+
+ if (!this->polylines_only) {
+ this->_setCtrl(p, mevent.state);
+ } else {
+ this->_setCtrl(this->p[1], mevent.state);
+ }
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ ret = true;
+ break;
+ case PenTool::STOP:
+ // Don't break; fall through to default to do preSnapping
+ default:
+ if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ // calls the function "bspline_spiro_motion" when the mouse starts or stops moving
+ if(this->bspline){
+ this->_bsplineSpiroMotion(mevent.state);
+ }else{
+ if ( Geom::LInfty( event_w - pen_drag_origin_w ) > (tolerance/2) || mevent.time == 0) {
+ this->_bsplineSpiroMotion(mevent.state);
+ pen_drag_origin_w = event_w;
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * Handle mouse button release event.
+ */
+bool PenTool::_handleButtonRelease(GdkEventButton const &revent) {
+ if (this->events_disabled) {
+ // skip event processing if events are disabled
+ return false;
+ }
+
+ bool ret = false;
+
+ if (revent.button == 1 && !this->space_panning) {
+ Geom::Point const event_w(revent.x, revent.y);
+
+ // Find desktop coordinates
+ Geom::Point p = this->desktop->w2d(event_w);
+
+ // Test whether we hit any anchor.
+
+ SPDrawAnchor *anchor = spdc_test_inside(this, event_w);
+ // if we try to create a node in the same place as another node, we skip
+ if((!anchor || anchor == this->sa) && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){
+ return true;
+ }
+
+ switch (this->mode) {
+ case PenTool::MODE_CLICK:
+ switch (this->state) {
+ case PenTool::POINT:
+ this->ea = anchor;
+ if (anchor) {
+ p = anchor->dp;
+ }
+ this->state = PenTool::CONTROL;
+ break;
+ case PenTool::CONTROL:
+ // End current segment
+ this->_endpointSnap(p, revent.state);
+ this->_finishSegment(p, revent.state);
+ this->state = PenTool::POINT;
+ break;
+ case PenTool::CLOSE:
+ // End current segment
+ if (!anchor) { // Snap node only if not hitting anchor
+ this->_endpointSnap(p, revent.state);
+ }
+ this->_finishSegment(p, revent.state);
+ // hude the guide of the penultimate node when closing the curve
+ if(this->spiro){
+ sp_canvas_item_hide(this->c1);
+ }
+ this->_finish(true);
+ this->state = PenTool::POINT;
+ break;
+ case PenTool::STOP:
+ // This is allowed, if we just canceled curve
+ this->state = PenTool::POINT;
+ break;
+ default:
+ break;
+ }
+ break;
+ case PenTool::MODE_DRAG:
+ switch (this->state) {
+ case PenTool::POINT:
+ case PenTool::CONTROL:
+ this->_endpointSnap(p, revent.state);
+ this->_finishSegment(p, revent.state);
+ break;
+ case PenTool::CLOSE:
+ this->_endpointSnap(p, revent.state);
+ this->_finishSegment(p, revent.state);
+ // hide the penultimate node guide when closing the curve
+ if(this->spiro){
+ sp_canvas_item_hide(this->c1);
+ }
+ if (this->green_closed) {
+ // finishing at the start anchor, close curve
+ this->_finish(true);
+ } else {
+ // finishing at some other anchor, finish curve but not close
+ this->_finish(false);
+ }
+ break;
+ case PenTool::STOP:
+ // This is allowed, if we just cancelled curve
+ break;
+ default:
+ break;
+ }
+ this->state = PenTool::POINT;
+ break;
+ default:
+ break;
+ }
+ if (this->grab) {
+ // Release grab now
+ sp_canvas_item_ungrab(this->grab);
+ this->grab = nullptr;
+ }
+
+ ret = true;
+
+ this->green_closed = false;
+ }
+
+ // TODO: can we be sure that the path was created correctly?
+ // TODO: should we offer an option to collect the clicks in a list?
+ if (this->expecting_clicks_for_LPE == 0 && this->hasWaitingLPE()) {
+ this->setPolylineMode();
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ if (this->waiting_LPE) {
+ // we have an already created LPE waiting for a path
+ this->waiting_LPE->acceptParamPath(SP_PATH(selection->singleItem()));
+ selection->add(this->waiting_item);
+ this->waiting_LPE = nullptr;
+ } else {
+ // the case that we need to create a new LPE and apply it to the just-drawn path is
+ // handled in spdc_check_for_and_apply_waiting_LPE() in draw-context.cpp
+ }
+ }
+
+ return ret;
+}
+
+bool PenTool::_handle2ButtonPress(GdkEventButton const &bevent) {
+ bool ret = false;
+ // only end on LMB double click. Otherwise horizontal scrolling causes ending of the path
+ if (this->npoints != 0 && bevent.button == 1 && this->state != PenTool::CLOSE) {
+ this->_finish(false);
+ ret = true;
+ }
+ return ret;
+}
+
+void PenTool::_redrawAll() {
+ // green
+ if (! this->green_bpaths.empty()) {
+ // remove old piecewise green canvasitems
+ for (auto i : this->green_bpaths){
+ sp_canvas_item_destroy(i);
+ }
+ this->green_bpaths.clear();
+ // one canvas bpath for all of green_curve
+ SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO);
+
+ this->green_bpaths.push_back(canvas_shape);
+ }
+ if (this->green_anchor) {
+ SP_CTRL(this->green_anchor->ctrl)->moveto(this->green_anchor->dp);
+ }
+
+ this->red_curve->reset();
+ this->red_curve->moveto(this->p[0]);
+ this->red_curve->curveto(this->p[1], this->p[2], this->p[3]);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+
+ // handles
+ // hide the handlers in bspline and spiro modes
+ if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) {
+ SP_CTRL(this->c1)->moveto(this->p[1]);
+ this->cl1->setCoords(this->p[0], this->p[1]);
+ sp_canvas_item_show(this->c1);
+ sp_canvas_item_show(this->cl1);
+ } else {
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl1);
+ }
+
+ Geom::Curve const * last_seg = this->green_curve->last_segment();
+ if (last_seg) {
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( last_seg );
+ // hide the handlers in bspline and spiro modes
+ if ( cubic &&
+ (*cubic)[2] != this->p[0] && !this->spiro && !this->bspline )
+ {
+ Geom::Point p2 = (*cubic)[2];
+ SP_CTRL(this->c0)->moveto(p2);
+ this->cl0->setCoords(p2, this->p[0]);
+ sp_canvas_item_show(this->c0);
+ sp_canvas_item_show(this->cl0);
+ } else {
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->cl0);
+ }
+ }
+
+ // simply redraw the spiro. because its a redrawing, we don't call the global function,
+ // but we call the redrawing at the ending.
+ this->_bsplineSpiroBuild();
+}
+
+void PenTool::_lastpointMove(gdouble x, gdouble y) {
+ if (this->npoints != 5)
+ return;
+
+ y *= -this->desktop->yaxisdir();
+
+ // green
+ if (!this->green_curve->is_unset()) {
+ this->green_curve->last_point_additive_move( Geom::Point(x,y) );
+ } else {
+ // start anchor too
+ if (this->green_anchor) {
+ this->green_anchor->dp += Geom::Point(x, y);
+ }
+ }
+
+ // red
+
+ this->p[0] += Geom::Point(x, y);
+ this->p[1] += Geom::Point(x, y);
+ this->_redrawAll();
+}
+
+void PenTool::_lastpointMoveScreen(gdouble x, gdouble y) {
+ this->_lastpointMove(x / this->desktop->current_zoom(), y / this->desktop->current_zoom());
+}
+
+void PenTool::_lastpointToCurve() {
+ // avoid that if the "red_curve" contains only two points ( rect ), it doesn't stop here.
+ if (this->npoints != 5 && !this->spiro && !this->bspline)
+ return;
+
+ this->p[1] = this->red_curve->last_segment()->initialPoint() + (1./3.)*(*this->red_curve->last_point() - this->red_curve->last_segment()->initialPoint());
+ //modificate the last segment of the green curve so it creates the type of node we need
+ if (this->spiro||this->bspline) {
+ if (!this->green_curve->is_unset()) {
+ Geom::Point A(0,0);
+ Geom::Point B(0,0);
+ Geom::Point C(0,0);
+ Geom::Point D(0,0);
+ Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() );
+ //We obtain the last segment 4 points in the previous curve
+ if ( cubic ){
+ A = (*cubic)[0];
+ B = (*cubic)[1];
+ if (this->spiro) {
+ C = this->p[0] + (this->p[0] - this->p[1]);
+ } else {
+ C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point());
+ }
+ D = (*cubic)[3];
+ } else {
+ A = this->green_curve->last_segment()->initialPoint();
+ B = this->green_curve->last_segment()->initialPoint();
+ if (this->spiro) {
+ C = this->p[0] + (this->p[0] - this->p[1]);
+ } else {
+ C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point());
+ }
+ D = *this->green_curve->last_point();
+ }
+ SPCurve *previous = new SPCurve();
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if ( this->green_curve->get_segment_count() == 1) {
+ this->green_curve = previous;
+ } else {
+ //we eliminate the last segment
+ this->green_curve->backspace();
+ //and we add it again with the recreation
+ this->green_curve->append_continuous(previous, 0.0625);
+ }
+ }
+ //if the last node is an union with another curve
+ if (this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()) {
+ this->_bsplineSpiroStartAnchor(false);
+ }
+ }
+
+ this->_redrawAll();
+}
+
+
+void PenTool::_lastpointToLine() {
+ // avoid that if the "red_curve" contains only two points ( rect) it doesn't stop here.
+ if (this->npoints != 5 && !this->bspline)
+ return;
+
+ // modify the last segment of the green curve so the type of node we want is created.
+ if(this->spiro || this->bspline){
+ if(!this->green_curve->is_unset()){
+ Geom::Point A(0,0);
+ Geom::Point B(0,0);
+ Geom::Point C(0,0);
+ Geom::Point D(0,0);
+ SPCurve * previous = new SPCurve();
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() );
+ if ( cubic ) {
+ A = this->green_curve->last_segment()->initialPoint();
+ B = (*cubic)[1];
+ C = *this->green_curve->last_point();
+ D = C;
+ } else {
+ //We obtain the last segment 4 points in the previous curve
+ A = this->green_curve->last_segment()->initialPoint();
+ B = A;
+ C = *this->green_curve->last_point();
+ D = C;
+ }
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if( this->green_curve->get_segment_count() == 1){
+ this->green_curve = previous;
+ }else{
+ //we eliminate the last segment
+ this->green_curve->backspace();
+ //and we add it again with the recreation
+ this->green_curve->append_continuous(previous, 0.0625);
+ }
+ }
+ // if the last node is an union with another curve
+ if(this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()){
+ this->_bsplineSpiroStartAnchor(true);
+ }
+ }
+
+ this->p[1] = this->p[0];
+ this->_redrawAll();
+}
+
+
+bool PenTool::_handleKeyPress(GdkEvent *event) {
+ bool ret = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+
+ // Check for undo if we have started drawing a path.
+ if (this->npoints > 0) {
+ unsigned int shortcut = sp_shortcut_get_for_event((GdkEventKey*)event);
+ Inkscape::Verb* verb = sp_shortcut_get_verb(shortcut);
+ if (verb) {
+ unsigned int vcode = verb->get_code();
+ if (vcode == SP_VERB_EDIT_UNDO)
+ return _undoLastPoint();
+ }
+ }
+
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Left: // move last point left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(-10, 0); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(-1, 0); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(-10*nudge, 0); // shift
+ }
+ else {
+ this->_lastpointMove(-nudge, 0); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Up: // move last point up
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(0, 10); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(0, 1); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(0, 10*nudge); // shift
+ }
+ else {
+ this->_lastpointMove(0, nudge); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Right: // move last point right
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(10, 0); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(1, 0); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(10*nudge, 0); // shift
+ }
+ else {
+ this->_lastpointMove(nudge, 0); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Down: // move last point down
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(0, -10); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(0, -1); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(0, -10*nudge); // shift
+ }
+ else {
+ this->_lastpointMove(0, -nudge); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+
+/*TODO: this is not yet enabled?? looks like some traces of the Geometry tool
+ case GDK_KEY_P:
+ case GDK_KEY_p:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PARALLEL, 2);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_C:
+ case GDK_KEY_c:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::CIRCLE_3PTS, 3);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_B:
+ case GDK_KEY_b:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PERP_BISECTOR, 2);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_A:
+ case GDK_KEY_a:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::ANGLE_BISECTOR, 3);
+ ret = true;
+ }
+ break;
+*/
+
+ case GDK_KEY_U:
+ case GDK_KEY_u:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->_lastpointToCurve();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_L:
+ case GDK_KEY_l:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->_lastpointToLine();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (this->npoints != 0) {
+ this->ea = nullptr; // unset end anchor if set (otherwise crashes)
+ if(MOD__SHIFT_ONLY(event)) {
+ // All this is needed to stop the last control
+ // point dispeating and stop making an n-1 shape.
+ Geom::Point const p(0, 0);
+ if(this->red_curve->is_unset()) {
+ this->red_curve->moveto(p);
+ }
+ this->_finishSegment(p, 0);
+ this->_finish(true);
+ } else {
+ this->_finish(false);
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Escape:
+ if (this->npoints != 0) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->_cancel ();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_BackSpace:
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ ret = _undoLastPoint();
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+void PenTool::_resetColors() {
+ // Red
+ this->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr, true);
+ // Blue
+ this->blue_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->blue_bpath), nullptr, true);
+ // Green
+ for (auto i:this->green_bpaths) {
+ sp_canvas_item_destroy(i);
+ }
+ this->green_bpaths.clear();
+ this->green_curve->reset();
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+ this->sa = nullptr;
+ this->ea = nullptr;
+ this->sa_overwrited->reset();
+
+ this->npoints = 0;
+ this->red_curve_is_valid = false;
+}
+
+
+void PenTool::_setInitialPoint(Geom::Point const p) {
+ g_assert( this->npoints == 0 );
+
+ this->p[0] = p;
+ this->p[1] = p;
+ this->npoints = 2;
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr, true);
+
+ this->desktop->canvas->forceFullRedrawAfterInterruptions(5);
+}
+
+/**
+ * Show the status message for the current line/curve segment.
+ * This type of message always shows angle/distance as the last
+ * two parameters ("angle %3.2f&#176;, distance %s").
+ */
+void PenTool::_setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message) {
+ g_assert((pc_point_to_compare == 0) || (pc_point_to_compare == 3)); // exclude control handles
+ g_assert(message != nullptr);
+
+ Geom::Point rel = p - this->p[pc_point_to_compare];
+ Inkscape::Util::Quantity q = Inkscape::Util::Quantity(Geom::L2(rel), "px");
+ Glib::ustring dist = q.string(desktop->namedview->display_units);
+ double angle = atan2(rel[Geom::Y], rel[Geom::X]) * 180 / M_PI;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/compassangledisplay/value", false) != 0) {
+ angle = 90 - angle;
+
+ if (desktop->is_yaxisdown()) {
+ angle = 180 - angle;
+ }
+
+ if (angle < 0) {
+ angle += 360;
+ }
+ }
+
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, message, angle, dist.c_str());
+}
+
+// this function changes the colors red, green and blue making them transparent or not, depending on if spiro is being used.
+void PenTool::_bsplineSpiroColor()
+{
+ static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if(this->spiro){
+ this->red_color = 0xff000000;
+ this->green_color = 0x00ff0000;
+ }else if(this->bspline){
+ this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color();
+ if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){
+ this->green_color = 0xff00007f;
+ this->red_color = 0xff00007f;
+ } else {
+ this->green_color = this->highlight_color;
+ this->red_color = this->highlight_color;
+ }
+ }else{
+ this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color();
+ this->red_color = 0xff00007f;
+ if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){
+ this->green_color = 0x00ff007f;
+ } else {
+ this->green_color = this->highlight_color;
+ }
+ sp_canvas_item_hide(this->blue_bpath);
+ }
+ //We erase all the "green_bpaths" to recreate them after with the colour
+ //transparency recently modified
+ if (!this->green_bpaths.empty()) {
+ // remove old piecewise green canvasitems
+ for (auto i:this->green_bpaths) {
+ sp_canvas_item_destroy(i);
+ }
+ this->green_bpaths.clear();
+ // one canvas bpath for all of green_curve
+ SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_back(canvas_shape);
+ }
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+}
+
+
+void PenTool::_bsplineSpiro(bool shift)
+{
+ if(!this->spiro && !this->bspline){
+ return;
+ }
+
+ shift?this->_bsplineSpiroOff():this->_bsplineSpiroOn();
+ this->_bsplineSpiroBuild();
+}
+
+void PenTool::_bsplineSpiroOn()
+{
+ if(!this->red_curve->is_unset()){
+ using Geom::X;
+ using Geom::Y;
+ this->npoints = 5;
+ this->p[0] = *this->red_curve->first_point();
+ this->p[3] = this->red_curve->first_segment()->finalPoint();
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ }
+}
+
+void PenTool::_bsplineSpiroOff()
+{
+ if(!this->red_curve->is_unset()){
+ this->npoints = 5;
+ this->p[0] = *this->red_curve->first_point();
+ this->p[3] = this->red_curve->first_segment()->finalPoint();
+ this->p[2] = this->p[3];
+ }
+}
+
+void PenTool::_bsplineSpiroStartAnchor(bool shift)
+{
+ if(this->sa->curve->is_unset()){
+ return;
+ }
+
+ LivePathEffect::LPEBSpline *lpe_bsp = nullptr;
+
+ if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect* thisEffect = SP_LPE_ITEM(this->white_item)->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if(thisEffect){
+ lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline*>(thisEffect->getLPEObj()->get_lpe());
+ }
+ }
+ if(lpe_bsp){
+ this->bspline = true;
+ }else{
+ this->bspline = false;
+ }
+ LivePathEffect::LPESpiro *lpe_spi = nullptr;
+
+ if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect* thisEffect = SP_LPE_ITEM(this->white_item)->getPathEffectOfType(Inkscape::LivePathEffect::SPIRO);
+ if(thisEffect){
+ lpe_spi = dynamic_cast<LivePathEffect::LPESpiro*>(thisEffect->getLPEObj()->get_lpe());
+ }
+ }
+ if(lpe_spi){
+ this->spiro = true;
+ }else{
+ this->spiro = false;
+ }
+ if(!this->spiro && !this->bspline){
+ _bsplineSpiroColor();
+ return;
+ }
+ if(shift){
+ this->_bsplineSpiroStartAnchorOff();
+ } else {
+ this->_bsplineSpiroStartAnchorOn();
+ }
+}
+
+void PenTool::_bsplineSpiroStartAnchorOn()
+{
+ using Geom::X;
+ using Geom::Y;
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited ->last_segment());
+ SPCurve *last_segment = new SPCurve();
+ Geom::Point point_a = this->sa_overwrited->last_segment()->initialPoint();
+ Geom::Point point_d = *this->sa_overwrited->last_point();
+ Geom::Point point_c = point_d + (1./3)*(point_a - point_d);
+ point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP);
+ if(cubic){
+ last_segment->moveto(point_a);
+ last_segment->curveto((*cubic)[1],point_c,point_d);
+ }else{
+ last_segment->moveto(point_a);
+ last_segment->curveto(point_a,point_c,point_d);
+ }
+ if( this->sa_overwrited->get_segment_count() == 1){
+ this->sa_overwrited = last_segment->copy();
+ }else{
+ //we eliminate the last segment
+ this->sa_overwrited->backspace();
+ //and we add it again with the recreation
+ this->sa_overwrited->append_continuous(last_segment, 0.0625);
+ }
+ last_segment->unref();
+}
+
+void PenTool::_bsplineSpiroStartAnchorOff()
+{
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited->last_segment());
+ if(cubic){
+ SPCurve *last_segment = new SPCurve();
+ last_segment->moveto((*cubic)[0]);
+ last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]);
+ if( this->sa_overwrited->get_segment_count() == 1){
+ this->sa_overwrited = last_segment->copy();
+ }else{
+ //we eliminate the last segment
+ this->sa_overwrited->backspace();
+ //and we add it again with the recreation
+ this->sa_overwrited->append_continuous(last_segment, 0.0625);
+ }
+ last_segment->unref();
+ }
+}
+
+void PenTool::_bsplineSpiroMotion(guint const state){
+ bool shift = state & GDK_SHIFT_MASK;
+ if(!this->spiro && !this->bspline){
+ return;
+ }
+ using Geom::X;
+ using Geom::Y;
+ if(this->red_curve->is_unset()) return;
+ this->npoints = 5;
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ if (this->green_curve->is_unset() && !this->sa) {
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP, this->p[1][Y] + HANDLE_CUBIC_GAP);
+ if(shift){
+ this->p[2] = this->p[3];
+ }
+ } else if (!this->green_curve->is_unset()){
+ tmp_curve.reset(this->green_curve->copy());
+ } else {
+ tmp_curve.reset(this->sa_overwrited->copy());
+ }
+ if ((state & GDK_MOD1_MASK ) && previous != Geom::Point(0,0)) { //ALT drag
+ this->p[0] = this->p[0] + (this->p[3] - previous);
+ }
+ if(!tmp_curve ->is_unset()){
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if ((state & GDK_MOD1_MASK ) &&
+ !Geom::are_near(*tmp_curve ->last_point(), this->p[0], 0.1))
+ {
+ SPCurve * previous_weight_power = new SPCurve();
+ Geom::D2< Geom::SBasis > SBasisweight_power;
+ previous_weight_power->moveto(tmp_curve ->last_segment()->initialPoint());
+ previous_weight_power->lineto(this->p[0]);
+ SBasisweight_power = previous_weight_power->first_segment()->toSBasis();
+ previous_weight_power->reset();
+ previous_weight_power->unref();
+ if( tmp_curve ->get_segment_count() == 1){
+ Geom::Point initial = tmp_curve ->last_segment()->initialPoint();
+ tmp_curve->reset();
+ tmp_curve->moveto(initial);
+ }else{
+ tmp_curve->backspace();
+ }
+ if(this->bspline && cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])){
+ tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), SBasisweight_power.valueAt(0.66667), this->p[0]);
+ } else if(this->bspline && cubic) {
+ tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), this->p[0], this->p[0]);
+ } else if (cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])) {
+ tmp_curve->curveto((*cubic)[1], (*cubic)[2] + (this->p[3] - previous), this->p[0]);
+ } else if (cubic){
+ tmp_curve->curveto((*cubic)[1], this->p[0], this->p[0]);
+ } else {
+ tmp_curve->lineto(this->p[0]);
+ }
+ cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if (this->sa && this->green_curve->is_unset()) {
+ this->sa_overwrited = tmp_curve->copy();
+ }
+ this->green_curve = tmp_curve->copy();
+ }
+ if (cubic) {
+ if (this->bspline) {
+ SPCurve * weight_power = new SPCurve();
+ Geom::D2< Geom::SBasis > SBasisweight_power;
+ weight_power->moveto(this->red_curve->last_segment()->initialPoint());
+ weight_power->lineto(*this->red_curve->last_point());
+ SBasisweight_power = weight_power->first_segment()->toSBasis();
+ weight_power->reset();
+ weight_power->unref();
+ this->p[1] = SBasisweight_power.valueAt(0.33334);
+ if(!Geom::are_near(this->p[1],this->p[0])){
+ this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP,this->p[1][Y] + HANDLE_CUBIC_GAP);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ if(Geom::are_near((*cubic)[3], (*cubic)[2])) {
+ this->p[1] = this->p[0];
+ }
+ } else {
+ this->p[1] = (*cubic)[3] + ((*cubic)[3] - (*cubic)[2] );
+ }
+ } else {
+ this->p[1] = this->p[0];
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ }
+ previous = *this->red_curve->last_point();
+ SPCurve * red = new SPCurve();
+ red->moveto(this->p[0]);
+ red->curveto(this->p[1],this->p[2],this->p[3]);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), red, true);
+ red->reset();
+ red->unref();
+ }
+
+ if(this->anchor_statusbar && !this->red_curve->is_unset()){
+ if(shift){
+ this->_bsplineSpiroEndAnchorOff();
+ }else{
+ this->_bsplineSpiroEndAnchorOn();
+ }
+ }
+ if (!this->green_bpaths.empty()) {
+ // remove old piecewise green canvasitems
+ for (auto i: this->green_bpaths) {
+ sp_canvas_item_destroy(i);
+ }
+ this->green_bpaths.clear();
+ }
+ // one canvas bpath for all of green_curve
+ SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_back(canvas_shape);
+ this->_bsplineSpiroBuild();
+}
+
+void PenTool::_bsplineSpiroEndAnchorOn()
+{
+
+ using Geom::X;
+ using Geom::Y;
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ std::unique_ptr<SPCurve> last_segment(new SPCurve());
+ Geom::Point point_c(0,0);
+ if( this->green_anchor && this->green_anchor->active ){
+ tmp_curve.reset(this->green_curve->create_reverse());
+ if(this->green_curve->get_segment_count()==0){
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve.reset(this->sa_overwrited->copy()->create_reverse());
+ }else{
+ return;
+ }
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if(this->bspline){
+ point_c = *tmp_curve ->last_point() + (1./3)*(tmp_curve ->last_segment()->initialPoint() - *tmp_curve ->last_point());
+ point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP);
+ }else{
+ point_c = this->p[3] + this->p[3] - this->p[2];
+ }
+ if(cubic){
+ last_segment->moveto((*cubic)[0]);
+ last_segment->curveto((*cubic)[1],point_c,(*cubic)[3]);
+ }else{
+ last_segment->moveto(tmp_curve ->last_segment()->initialPoint());
+ last_segment->lineto(*tmp_curve ->last_point());
+ }
+ if( tmp_curve ->get_segment_count() == 1){
+ tmp_curve = std::move(last_segment);
+ }else{
+ //we eliminate the last segment
+ tmp_curve ->backspace();
+ //and we add it again with the recreation
+ tmp_curve ->append_continuous(last_segment.get(), 0.0625);
+ }
+ tmp_curve.reset(tmp_curve ->create_reverse());
+ if( this->green_anchor && this->green_anchor->active )
+ {
+ this->green_curve->reset();
+ this->green_curve = tmp_curve->copy();
+ }else{
+ this->sa_overwrited->reset();
+ this->sa_overwrited = tmp_curve->copy();
+ }
+}
+
+void PenTool::_bsplineSpiroEndAnchorOff()
+{
+
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ std::unique_ptr<SPCurve> last_segment(new SPCurve());
+ this->p[2] = this->p[3];
+ if( this->green_anchor && this->green_anchor->active ){
+ tmp_curve.reset(this->green_curve->create_reverse());
+ if(this->green_curve->get_segment_count()==0){
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve.reset(this->sa_overwrited->copy()->create_reverse());
+ }else{
+ return;
+ }
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if(cubic){
+ last_segment->moveto((*cubic)[0]);
+ last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]);
+ }else{
+ last_segment->moveto(tmp_curve ->last_segment()->initialPoint());
+ last_segment->lineto(*tmp_curve ->last_point());
+ }
+ if( tmp_curve ->get_segment_count() == 1){
+ tmp_curve = std::move(last_segment);
+ }else{
+ //we eliminate the last segment
+ tmp_curve ->backspace();
+ //and we add it again with the recreation
+ tmp_curve ->append_continuous(last_segment.get(), 0.0625);
+ }
+ tmp_curve.reset(tmp_curve ->create_reverse());
+
+ if( this->green_anchor && this->green_anchor->active )
+ {
+ this->green_curve->reset();
+ this->green_curve = tmp_curve->copy();
+ }else{
+ this->sa_overwrited->reset();
+ this->sa_overwrited = tmp_curve->copy();
+ }
+}
+
+//prepares the curves for its transformation into BSpline curve.
+void PenTool::_bsplineSpiroBuild()
+{
+ if(!this->spiro && !this->bspline){
+ return;
+ }
+
+ //We create the base curve
+ SPCurve *curve = new SPCurve();
+ //If we continuate the existing curve we add it at the start
+ if(this->sa && !this->sa->curve->is_unset()){
+ delete curve;
+ curve = this->sa_overwrited->copy();
+ }
+
+ if (!this->green_curve->is_unset()){
+ curve->append_continuous(this->green_curve, 0.0625);
+ }
+
+ //and the red one
+ if (!this->red_curve->is_unset()){
+ this->red_curve->reset();
+ this->red_curve->moveto(this->p[0]);
+ if(this->anchor_statusbar && !this->sa && !(this->green_anchor && this->green_anchor->active)){
+ this->red_curve->curveto(this->p[1],this->p[3],this->p[3]);
+ }else{
+ this->red_curve->curveto(this->p[1],this->p[2],this->p[3]);
+ }
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+ curve->append_continuous(this->red_curve, 0.0625);
+ }
+ previous = *this->red_curve->last_point();
+ if(!curve->is_unset()){
+ // close the curve if the final points of the curve are close enough
+ if(Geom::are_near(curve->first_path()->initialPoint(), curve->last_path()->finalPoint())){
+ curve->closepath_current();
+ }
+ //TODO: CALL TO CLONED FUNCTION SPIRO::doEffect IN lpe-spiro.cpp
+ //For example
+ //using namespace Inkscape::LivePathEffect;
+ //LivePathEffectObject *lpeobj = static_cast<LivePathEffectObject*> (curve);
+ //Effect *spr = static_cast<Effect*> ( new LPEbspline(lpeobj) );
+ //spr->doEffect(curve);
+ if (this->bspline) {
+ Geom::PathVector hp;
+ LivePathEffect::sp_bspline_do_effect(curve, 0, hp);
+ } else {
+ LivePathEffect::sp_spiro_do_effect(curve);
+ }
+
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->blue_bpath), curve, true);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->blue_bpath), this->blue_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_show(this->blue_bpath);
+ curve->unref();
+ this->blue_curve->reset();
+ //We hide the holders that doesn't contribute anything
+ if(this->spiro){
+ sp_canvas_item_show(this->c1);
+ SP_CTRL(this->c1)->moveto(this->p[0]);
+ }else
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl1);
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->cl0);
+ }else{
+ //if the curve is empty
+ sp_canvas_item_hide(this->blue_bpath);
+
+ }
+}
+
+void PenTool::_setSubsequentPoint(Geom::Point const p, bool statusbar, guint status) {
+ g_assert( this->npoints != 0 );
+
+ // todo: Check callers to see whether 2 <= npoints is guaranteed.
+
+ this->p[2] = p;
+ this->p[3] = p;
+ this->p[4] = p;
+ this->npoints = 5;
+ this->red_curve->reset();
+ bool is_curve;
+ this->red_curve->moveto(this->p[0]);
+ if (this->polylines_paraxial && !statusbar) {
+ // we are drawing horizontal/vertical lines and hit an anchor;
+ Geom::Point const origin = this->p[0];
+ // if the previous point and the anchor are not aligned either horizontally or vertically...
+ if ((std::abs(p[Geom::X] - origin[Geom::X]) > 1e-9) && (std::abs(p[Geom::Y] - origin[Geom::Y]) > 1e-9)) {
+ // ...then we should draw an L-shaped path, consisting of two paraxial segments
+ Geom::Point intermed = p;
+ this->_setToNearestHorizVert(intermed, status);
+ this->red_curve->lineto(intermed);
+ }
+ this->red_curve->lineto(p);
+ is_curve = false;
+ } else {
+ // one of the 'regular' modes
+ if (this->p[1] != this->p[0] || this->spiro) {
+ this->red_curve->curveto(this->p[1], p, p);
+ is_curve = true;
+ } else {
+ this->red_curve->lineto(p);
+ is_curve = false;
+ }
+ }
+
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+
+ if (statusbar) {
+ gchar *message;
+ if(this->spiro || this->bspline){
+ message = is_curve ?
+ _("<b>Curve segment</b>: angle %3.2f&#176;; with <b>Shift+Click</b> cusp node,<b>ALT</b> move previous, <b>Enter</b> or <b>Shift+Enter</b> to finish" ):
+ _("<b>Line segment</b>: angle %3.2f&#176;; with <b>Shift+Click</b> cusp node,<b>ALT</b> move previous, <b>Enter</b> or <b>Shift+Enter</b> to finish");
+ this->_setAngleDistanceStatusMessage(p, 0, message);
+ } else {
+ message = is_curve ?
+ _("<b>Curve segment</b>: angle %3.2f&#176;, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path" ):
+ _("<b>Line segment</b>: angle %3.2f&#176;, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path");
+ this->_setAngleDistanceStatusMessage(p, 0, message);
+ }
+
+ }
+}
+
+
+void PenTool::_setCtrl(Geom::Point const p, guint const state) {
+ sp_canvas_item_show(this->c1);
+ sp_canvas_item_show(this->cl1);
+
+ if ( this->npoints == 2 ) {
+ this->p[1] = p;
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->cl0);
+ SP_CTRL(this->c1)->moveto(this->p[1]);
+ this->cl1->setCoords(this->p[0], this->p[1]);
+ this->_setAngleDistanceStatusMessage(p, 0, _("<b>Curve handle</b>: angle %3.2f&#176;, length %s; with <b>Ctrl</b> to snap angle"));
+ } else if ( this->npoints == 5 ) {
+ this->p[4] = p;
+ sp_canvas_item_show(this->c0);
+ sp_canvas_item_show(this->cl0);
+ bool is_symm = false;
+ if ( ( ( this->mode == PenTool::MODE_CLICK ) && ( state & GDK_CONTROL_MASK ) ) ||
+ ( ( this->mode == PenTool::MODE_DRAG ) && !( state & GDK_SHIFT_MASK ) ) ) {
+ Geom::Point delta = p - this->p[3];
+ this->p[2] = this->p[3] - delta;
+ is_symm = true;
+ this->red_curve->reset();
+ this->red_curve->moveto(this->p[0]);
+ this->red_curve->curveto(this->p[1], this->p[2], this->p[3]);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true);
+ }
+ SP_CTRL(this->c0)->moveto(this->p[2]);
+ this->cl0 ->setCoords(this->p[3], this->p[2]);
+ SP_CTRL(this->c1)->moveto(this->p[4]);
+ this->cl1->setCoords(this->p[3], this->p[4]);
+
+
+
+ gchar *message = is_symm ?
+ _("<b>Curve handle, symmetric</b>: angle %3.2f&#176;, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only") :
+ _("<b>Curve handle</b>: angle %3.2f&#176;, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only");
+ this->_setAngleDistanceStatusMessage(p, 3, message);
+ } else {
+ g_warning("Something bad happened - npoints is %d", this->npoints);
+ }
+}
+
+void PenTool::_finishSegment(Geom::Point const p, guint const state) {
+ if (this->polylines_paraxial) {
+ this->nextParaxialDirection(p, this->p[0], state);
+ }
+
+ ++num_clicks;
+
+
+ if (!this->red_curve->is_unset()) {
+ this->_bsplineSpiro(state & GDK_SHIFT_MASK);
+ if(!this->green_curve->is_unset() &&
+ !Geom::are_near(*this->green_curve->last_point(),this->p[0]))
+ {
+ std::unique_ptr<SPCurve> lsegment(new SPCurve());
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->green_curve->last_segment());
+ if (cubic) {
+ lsegment->moveto((*cubic)[0]);
+ lsegment->curveto((*cubic)[1], this->p[0] - ((*cubic)[2] - (*cubic)[3]), *this->red_curve->first_point());
+ this->green_curve->backspace();
+ this->green_curve->append_continuous(lsegment.get(), 0.0625);
+ }
+ }
+ this->green_curve->append_continuous(this->red_curve, 0.0625);
+ SPCurve *curve = this->red_curve->copy();
+
+ /// \todo fixme:
+ SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), curve, true);
+ curve->unref();
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ this->green_bpaths.push_back(canvas_shape);
+
+ this->p[0] = this->p[3];
+ this->p[1] = this->p[4];
+ this->npoints = 2;
+
+ this->red_curve->reset();
+ }
+}
+
+// Partial fix for https://bugs.launchpad.net/inkscape/+bug/171990
+// TODO: implement the redo feature
+bool PenTool::_undoLastPoint() {
+ bool ret = false;
+
+ if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) {
+ if (!this->red_curve->is_unset()) {
+ this->_cancel ();
+ ret = true;
+ } else {
+ // do nothing; this event should be handled upstream
+ }
+ } else {
+ // Reset red curve
+ this->red_curve->reset();
+ // Get last segment
+ if ( this->green_curve->is_unset() ) {
+ g_warning("pen_handle_key_press, case GDK_KP_Delete: Green curve is empty");
+ return false;
+ }
+ // The code below assumes that this->green_curve has only ONE path !
+ Geom::Curve const * crv = this->green_curve->last_segment();
+ this->p[0] = crv->initialPoint();
+ if ( Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>(crv)) {
+ this->p[1] = (*cubic)[1];
+
+ } else {
+ this->p[1] = this->p[0];
+ }
+
+ // assign the value in a third of the distance of the last segment.
+ if (this->bspline){
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ }
+
+ Geom::Point const pt( (this->npoints < 4) ? crv->finalPoint() : this->p[3] );
+
+ this->npoints = 2;
+ // delete the last segment of the green curve and green bpath
+ if (this->green_curve->get_segment_count() == 1) {
+ this->npoints = 5;
+ if (!this->green_bpaths.empty()) {
+ sp_canvas_item_destroy(this->green_bpaths.back());
+ this->green_bpaths.pop_back();
+ }
+ this->green_curve->reset();
+ } else {
+ this->green_curve->backspace();
+ if (this->green_bpaths.size() > 1) {
+ sp_canvas_item_destroy(this->green_bpaths.back());
+ this->green_bpaths.pop_back();
+ } else if (this->green_bpaths.size() == 1) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(green_bpaths.back()), this->green_curve, true);
+ }
+ }
+
+ // assign the value of this->p[1] to the opposite of the green line last segment
+ if (this->spiro){
+ Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(this->green_curve->last_segment());
+ if ( cubic ) {
+ this->p[1] = (*cubic)[3] + (*cubic)[3] - (*cubic)[2];
+ SP_CTRL(this->c1)->moveto(this->p[0]);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ }
+
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl0);
+ sp_canvas_item_hide(this->cl1);
+ this->state = PenTool::POINT;
+
+ if(this->polylines_paraxial) {
+ // We compare the point we're removing with the nearest horiz/vert to
+ // see if the line was added with SHIFT or not.
+ Geom::Point compare(pt);
+ this->_setToNearestHorizVert(compare, 0);
+ if ((std::abs(compare[Geom::X] - pt[Geom::X]) > 1e-9)
+ || (std::abs(compare[Geom::Y] - pt[Geom::Y]) > 1e-9)) {
+ this->paraxial_angle = this->paraxial_angle.cw();
+ }
+ }
+ this->_setSubsequentPoint(pt, true);
+
+ //redraw
+ this->_bsplineSpiroBuild();
+ ret = true;
+ }
+
+ return ret;
+}
+
+void PenTool::_finish(gboolean const closed) {
+ if (this->expecting_clicks_for_LPE > 1) {
+ // don't let the path be finished before we have collected the required number of mouse clicks
+ return;
+ }
+
+
+ this->num_clicks = 0;
+
+ this->_disableEvents();
+
+ this->message_context->clear();
+
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Drawing finished"));
+
+ // cancelate line without a created segment
+ this->red_curve->reset();
+ spdc_concat_colors_and_flush(this, closed);
+ this->sa = nullptr;
+ this->ea = nullptr;
+
+ this->npoints = 0;
+ this->state = PenTool::POINT;
+
+ sp_canvas_item_hide(this->c0);
+ sp_canvas_item_hide(this->c1);
+ sp_canvas_item_hide(this->cl0);
+ sp_canvas_item_hide(this->cl1);
+
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+
+ this->desktop->canvas->endForcedFullRedraws();
+
+ this->_enableEvents();
+}
+
+void PenTool::_disableEvents() {
+ this->events_disabled = true;
+}
+
+void PenTool::_enableEvents() {
+ g_return_if_fail(this->events_disabled != 0);
+
+ this->events_disabled = false;
+}
+
+void PenTool::waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines) {
+ if (effect_type == Inkscape::LivePathEffect::INVALID_LPE)
+ return;
+
+ this->waiting_LPE_type = effect_type;
+ this->expecting_clicks_for_LPE = num_clicks;
+ this->polylines_only = use_polylines;
+ this->polylines_paraxial = false; // TODO: think if this is correct for all cases
+}
+
+void PenTool::nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state) {
+ //
+ // after the first mouse click we determine whether the mouse pointer is closest to a
+ // horizontal or vertical segment; for all subsequent mouse clicks, we use the direction
+ // orthogonal to the last one; pressing Shift toggles the direction
+ //
+ // num_clicks is not reliable because spdc_pen_finish_segment is sometimes called too early
+ // (on first mouse release), in which case num_clicks immediately becomes 1.
+ // if (this->num_clicks == 0) {
+
+ if (this->green_curve->is_unset()) {
+ // first mouse click
+ double h = pt[Geom::X] - origin[Geom::X];
+ double v = pt[Geom::Y] - origin[Geom::Y];
+ this->paraxial_angle = Geom::Point(h, v).ccw();
+ }
+ if(!(state & GDK_SHIFT_MASK)) {
+ this->paraxial_angle = this->paraxial_angle.ccw();
+ }
+}
+
+void PenTool::_setToNearestHorizVert(Geom::Point &pt, guint const state) const {
+ Geom::Point const origin = this->p[0];
+ Geom::Point const target = (state & GDK_SHIFT_MASK) ? this->paraxial_angle : this->paraxial_angle.ccw();
+
+ // Create a horizontal or vertical constraint line
+ Inkscape::Snapper::SnapConstraint cl(origin, target);
+
+ // Snap along the constraint line; if we didn't snap then still the constraint will be applied
+ SnapManager &m = this->desktop->namedview->snap_manager;
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+ // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping)
+ // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment
+
+ m.setup(this->desktop, true, selection->singleItem());
+ m.constrainedSnapReturnByRef(pt, Inkscape::SNAPSOURCE_NODE_HANDLE, cl);
+ m.unSetup();
+}
+
+}
+}
+}
+
+/*
+ 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/tools/pen-tool.h b/src/ui/tools/pen-tool.h
new file mode 100644
index 0000000..388bb3d
--- /dev/null
+++ b/src/ui/tools/pen-tool.h
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * PenTool: a context for pen tool events.
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_PEN_CONTEXT_H
+#define SEEN_PEN_CONTEXT_H
+
+
+
+#include "ui/tools/freehand-base.h"
+#include "live_effects/effect.h"
+
+#define SP_PEN_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PenTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_PEN_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PenTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+struct SPCtrlLine;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+/**
+ * PenTool: a context for pen tool events.
+ */
+class PenTool : public FreehandBase {
+public:
+ PenTool();
+ PenTool(gchar const *const *cursor_shape);
+ ~PenTool() override;
+
+ enum Mode {
+ MODE_CLICK,
+ MODE_DRAG
+ };
+
+ enum State {
+ POINT,
+ CONTROL,
+ CLOSE,
+ STOP
+ };
+
+ Geom::Point p[5];
+ Geom::Point previous;
+ /** \invar npoints in {0, 2, 5}. */
+ // npoints somehow determines the type of the node (what does it mean, exactly? the number of Bezier handles?)
+ gint npoints;
+
+ Mode mode;
+ State state;
+ bool polylines_only;
+ bool polylines_paraxial;
+ Geom::Point paraxial_angle;
+
+ // propiety which saves if Spiro mode is active or not
+ bool spiro;
+ bool bspline;
+ int num_clicks;
+
+ unsigned int expecting_clicks_for_LPE; // if positive, finish the path after this many clicks
+ Inkscape::LivePathEffect::Effect *waiting_LPE; // if NULL, waiting_LPE_type in SPDrawContext is taken into account
+ SPLPEItem *waiting_item;
+
+ SPCanvasItem *c0;
+ SPCanvasItem *c1;
+
+ SPCtrlLine *cl0;
+ SPCtrlLine *cl1;
+
+ bool events_disabled;
+
+ static const std::string prefsPath;
+
+ const std::string& getPrefsPath() override;
+
+ void nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state);
+ void setPolylineMode();
+ bool hasWaitingLPE();
+ void waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines = true);
+
+protected:
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+private:
+ bool _handleButtonPress(GdkEventButton const &bevent);
+ bool _handleMotionNotify(GdkEventMotion const &mevent);
+ bool _handleButtonRelease(GdkEventButton const &revent);
+ bool _handle2ButtonPress(GdkEventButton const &bevent);
+ bool _handleKeyPress(GdkEvent *event);
+ //this function changes the colors red, green and blue making them transparent or not depending on if the function uses spiro
+ void _bsplineSpiroColor();
+ //creates a node in bspline or spiro modes
+ void _bsplineSpiro(bool shift);
+ //creates a node in bspline or spiro modes
+ void _bsplineSpiroOn();
+ //creates a CUSP node
+ void _bsplineSpiroOff();
+ //continues the existing curve in bspline or spiro mode
+ void _bsplineSpiroStartAnchor(bool shift);
+ //continues the existing curve with the union node in bspline or spiro modes
+ void _bsplineSpiroStartAnchorOn();
+ //continues an existing curve with the union node in CUSP mode
+ void _bsplineSpiroStartAnchorOff();
+ //modifies the "red_curve" when it detects movement
+ void _bsplineSpiroMotion(guint const state);
+ //closes the curve with the last node in bspline or spiro mode
+ void _bsplineSpiroEndAnchorOn();
+ //closes the curve with the last node in CUSP mode
+ void _bsplineSpiroEndAnchorOff();
+ //apply the effect
+ void _bsplineSpiroBuild();
+
+ void _setInitialPoint(Geom::Point const p);
+ void _setSubsequentPoint(Geom::Point const p, bool statusbar, guint status = 0);
+ void _setCtrl(Geom::Point const p, guint state);
+ void _finishSegment(Geom::Point p, guint state);
+ bool _undoLastPoint();
+
+ void _finish(gboolean closed);
+
+ void _resetColors();
+
+ void _disableEvents();
+ void _enableEvents();
+
+ void _setToNearestHorizVert(Geom::Point &pt, guint const state) const;
+
+ void _setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message);
+
+ void _lastpointToLine();
+ void _lastpointToCurve();
+ void _lastpointMoveScreen(gdouble x, gdouble y);
+ void _lastpointMove(gdouble x, gdouble y);
+ void _redrawAll();
+
+ void _endpointSnapHandle(Geom::Point &p, guint const state) const;
+ void _endpointSnap(Geom::Point &p, guint const state) const;
+
+ void _cancel();
+};
+
+}
+}
+}
+
+#endif /* !SEEN_PEN_CONTEXT_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/tools/pencil-tool.cpp b/src/ui/tools/pencil-tool.cpp
new file mode 100644
index 0000000..7bb0275
--- /dev/null
+++ b/src/ui/tools/pencil-tool.cpp
@@ -0,0 +1,1239 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Pencil event context implementation.
+ */
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2004 Monash University
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdkkeysyms.h>
+
+#include "ui/tools/pencil-tool.h"
+#include <2geom/bezier-utils.h>
+#include <2geom/circle.h>
+#include <2geom/sbasis-to-bezier.h>
+#include <2geom/svg-path-parser.h>
+
+#include "desktop.h"
+#include "inkscape.h"
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "display/canvas-bpath.h"
+#include "display/curve.h"
+#include "display/sp-canvas.h"
+
+#include "livarot/Path.h" // Simplify paths
+
+#include "live_effects/lpe-powerstroke-interpolators.h"
+#include "live_effects/lpe-powerstroke.h"
+#include "live_effects/lpe-simplify.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-lpe-item.h"
+#include "object/sp-path.h"
+#include "splivarot.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-pencil.xpm"
+
+#include "svg/svg.h"
+
+#include "ui/draw-anchor.h"
+#include "ui/tool/event-utils.h"
+
+#include "xml/node.h"
+#include "xml/sp-css-attr.h"
+#include <glibmm/i18n.h>
+// #include <thread>
+// #include <chrono>
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static Geom::Point pencil_drag_origin_w(0, 0);
+static bool pencil_within_tolerance = false;
+
+static bool in_svg_plane(Geom::Point const &p) { return Geom::LInfty(p) < 1e18; }
+const double HANDLE_CUBIC_GAP = 0.01;
+
+const std::string& PencilTool::getPrefsPath() {
+ return PencilTool::prefsPath;
+}
+
+const std::string PencilTool::prefsPath = "/tools/freehand/pencil";
+
+PencilTool::PencilTool()
+ : FreehandBase(cursor_pencil_xpm)
+ , p()
+ , _npoints(0)
+ , _state(SP_PENCIL_CONTEXT_IDLE)
+ , _req_tangent(0, 0)
+ , _is_drawing(false)
+ , sketch_n(0)
+ , _curve(nullptr)
+ , _pressure_curve(nullptr)
+{
+}
+
+void PencilTool::setup() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/freehand/pencil/selcue")) {
+ this->enableSelectionCue();
+ }
+ this->_curve = new SPCurve();
+ this->_pressure_curve = new SPCurve();
+
+ FreehandBase::setup();
+
+ this->_is_drawing = false;
+ this->anchor_statusbar = false;
+}
+
+
+
+PencilTool::~PencilTool() {
+ if (this->_curve) {
+ this->_curve->unref();
+ }
+ if (this->_pressure_curve) {
+ this->_pressure_curve->unref();
+ }
+}
+
+void PencilTool::_extinput(GdkEvent *event) {
+ if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) {
+ this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE);
+ is_tablet = true;
+ } else {
+ this->pressure = DDC_DEFAULT_PRESSURE;
+ is_tablet = false;
+ }
+}
+
+/** Snaps new node relative to the previous node. */
+void PencilTool::_endpointSnap(Geom::Point &p, guint const state) {
+ if ((state & GDK_CONTROL_MASK)) { //CTRL enables constrained snapping
+ if (this->_npoints > 0) {
+ spdc_endpoint_snap_rotation(this, p, this->p[0], state);
+ }
+ } else {
+ if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above
+ //After all, the user explicitly asked for angular snapping by
+ //pressing CTRL
+ boost::optional<Geom::Point> origin = this->_npoints > 0 ? this->p[0] : boost::optional<Geom::Point>();
+ spdc_endpoint_snap_free(this, p, origin, state);
+ }
+ }
+}
+
+/**
+ * Callback for handling all pencil context events.
+ */
+bool PencilTool::root_handler(GdkEvent* event) {
+ bool ret = false;
+ this->_extinput(event);
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = this->_handleButtonPress(event->button);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ ret = this->_handleMotionNotify(event->motion);
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ ret = this->_handleButtonRelease(event->button);
+ break;
+
+ case GDK_KEY_PRESS:
+ ret = this->_handleKeyPress(event->key);
+ break;
+
+ case GDK_KEY_RELEASE:
+ ret = this->_handleKeyRelease(event->key);
+ break;
+
+ default:
+ break;
+ }
+ if (!ret) {
+ ret = FreehandBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+bool PencilTool::_handleButtonPress(GdkEventButton const &bevent) {
+ bool ret = false;
+ if ( bevent.button == 1 && !this->space_panning) {
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return true;
+ }
+
+ if (!this->grab) {
+ /* Grab mouse, so release will not pass unnoticed */
+ this->grab = SP_CANVAS_ITEM(desktop->acetate);
+ sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK ),
+ nullptr, bevent.time);
+ }
+
+ Geom::Point const button_w(bevent.x, bevent.y);
+
+ /* Find desktop coordinates */
+ Geom::Point p = this->desktop->w2d(button_w);
+
+ /* Test whether we hit any anchor. */
+ SPDrawAnchor *anchor = spdc_test_inside(this, button_w);
+ if (tablet_enabled) {
+ anchor = nullptr;
+ }
+ pencil_drag_origin_w = Geom::Point(bevent.x,bevent.y);
+ pencil_within_tolerance = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tablet_enabled = prefs->getBool("/tools/freehand/pencil/pressure", false);
+ switch (this->_state) {
+ case SP_PENCIL_CONTEXT_ADDLINE:
+ /* Current segment will be finished with release */
+ ret = true;
+ break;
+ default:
+ /* Set first point of sequence */
+ SnapManager &m = desktop->namedview->snap_manager;
+ if (bevent.state & GDK_CONTROL_MASK) {
+ m.setup(desktop, true);
+ if (!(bevent.state & GDK_SHIFT_MASK)) {
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ }
+ spdc_create_single_dot(this, p, "/tools/freehand/pencil", bevent.state);
+ m.unSetup();
+ ret = true;
+ break;
+ }
+ if (anchor) {
+ p = anchor->dp;
+ //Put the start overwrite curve always on the same direction
+ if (anchor->start) {
+ this->sa_overwrited = anchor->curve->create_reverse();
+ } else {
+ this->sa_overwrited = anchor->curve->copy();
+ }
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path"));
+ } else {
+ m.setup(desktop, true);
+ if (tablet_enabled) {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ selection->clear();
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ } else if (!(bevent.state & GDK_SHIFT_MASK)) {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ selection->clear();
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) {
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ }
+ m.unSetup();
+ }
+ if (!tablet_enabled) {
+ this->sa = anchor;
+ }
+ this->_setStartpoint(p);
+ ret = true;
+ break;
+ }
+
+ set_high_motion_precision();
+ this->_is_drawing = true;
+ }
+ return ret;
+}
+
+bool PencilTool::_handleMotionNotify(GdkEventMotion const &mevent) {
+ if ((mevent.state & GDK_CONTROL_MASK) && (mevent.state & GDK_BUTTON1_MASK)) {
+ // mouse was accidentally moved during Ctrl+click;
+ // ignore the motion and create a single point
+ this->_is_drawing = false;
+ return true;
+ }
+ bool ret = false;
+
+ if (this->space_panning || (mevent.state & GDK_BUTTON2_MASK) || (mevent.state & GDK_BUTTON3_MASK)) {
+ // allow scrolling
+ return ret;
+ }
+
+ /* Test whether we hit any anchor. */
+ SPDrawAnchor *anchor = spdc_test_inside(this, pencil_drag_origin_w);
+ if (this->pressure == 0.0 && tablet_enabled && !anchor) {
+ // tablet event was accidentally fired without press;
+ return ret;
+ }
+
+ if ( ( mevent.state & GDK_BUTTON1_MASK ) && !this->grab && this->_is_drawing) {
+ /* Grab mouse, so release will not pass unnoticed */
+ this->grab = SP_CANVAS_ITEM(desktop->acetate);
+ sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK ),
+ nullptr, mevent.time);
+ }
+
+ /* Find desktop coordinates */
+ Geom::Point p = desktop->w2d(Geom::Point(mevent.x, mevent.y));
+
+
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (pencil_within_tolerance) {
+ gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ if ( Geom::LInfty( Geom::Point(mevent.x,mevent.y) - pencil_drag_origin_w ) < tolerance ) {
+ return false; // Do not drag if we're within tolerance from origin.
+ }
+ }
+
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ pencil_within_tolerance = false;
+
+ anchor = spdc_test_inside(this, Geom::Point(mevent.x,mevent.y));
+
+ switch (this->_state) {
+ case SP_PENCIL_CONTEXT_ADDLINE:
+ if (is_tablet) {
+ this->_state = SP_PENCIL_CONTEXT_FREEHAND;
+ return false;
+ }
+ /* Set red endpoint */
+ if (anchor) {
+ p = anchor->dp;
+ } else {
+ Geom::Point ptnr(p);
+ this->_endpointSnap(ptnr, mevent.state);
+ p = ptnr;
+ }
+ this->_setEndpoint(p);
+ ret = true;
+ break;
+ default:
+ /* We may be idle or already freehand */
+ if ( (mevent.state & GDK_BUTTON1_MASK) && this->_is_drawing ) {
+ if (this->_state == SP_PENCIL_CONTEXT_IDLE) {
+ sp_event_context_discard_delayed_snap_event(this);
+ }
+ this->_state = SP_PENCIL_CONTEXT_FREEHAND;
+
+ if ( !this->sa && !this->green_anchor ) {
+ /* Create green anchor */
+ this->green_anchor = sp_draw_anchor_new(this, this->green_curve, TRUE, this->p[0]);
+ }
+ if (anchor) {
+ p = anchor->dp;
+ }
+ if ( this->_npoints != 0) { // buttonpress may have happened before we entered draw context!
+ if (this->ps.empty()) {
+ // Only in freehand mode we have to add the first point also to this->ps (apparently)
+ // - We cannot add this point in spdc_set_startpoint, because we only need it for freehand
+ // - We cannot do this in the button press handler because at that point we don't know yet
+ // whether we're going into freehand mode or not
+ this->ps.push_back(this->p[0]);
+ if (tablet_enabled) {
+ this->_wps.emplace_back(0, 0);
+ }
+ }
+ this->_addFreehandPoint(p, mevent.state, false);
+ ret = true;
+ }
+ if (anchor && !this->anchor_statusbar) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Release</b> here to close and finish the path."));
+ this->anchor_statusbar = true;
+ this->ea = anchor;
+ } else if (!anchor && this->anchor_statusbar) {
+ this->message_context->clear();
+ this->anchor_statusbar = false;
+ this->ea = nullptr;
+ } else if (!anchor) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Drawing a freehand path"));
+ this->ea = nullptr;
+ }
+
+ } else {
+ if (anchor && !this->anchor_statusbar) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag</b> to continue the path from this point."));
+ this->anchor_statusbar = true;
+ } else if (!anchor && this->anchor_statusbar) {
+ this->message_context->clear();
+ this->anchor_statusbar = false;
+ }
+ }
+
+ // Show the pre-snap indicator to communicate to the user where we would snap to if he/she were to
+ // a) press the mousebutton to start a freehand drawing, or
+ // b) release the mousebutton to finish a freehand drawing
+ if (!tablet_enabled && !this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true);
+ m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ }
+ return ret;
+}
+
+bool PencilTool::_handleButtonRelease(GdkEventButton const &revent) {
+ bool ret = false;
+
+ set_high_motion_precision(false);
+
+ if ( revent.button == 1 && this->_is_drawing && !this->space_panning) {
+ this->_is_drawing = false;
+
+ /* Find desktop coordinates */
+ Geom::Point p = desktop->w2d(Geom::Point(revent.x, revent.y));
+
+ /* Test whether we hit any anchor. */
+ SPDrawAnchor *anchor = spdc_test_inside(this, Geom::Point(revent.x, revent.y));
+
+ switch (this->_state) {
+ case SP_PENCIL_CONTEXT_IDLE:
+ /* Releasing button in idle mode means single click */
+ /* We have already set up start point/anchor in button_press */
+ if (!(revent.state & GDK_CONTROL_MASK) && !is_tablet) {
+ // Ctrl+click creates a single point so only set context in ADDLINE mode when Ctrl isn't pressed
+ this->_state = SP_PENCIL_CONTEXT_ADDLINE;
+ }
+ /*Or select the down item if we are in tablet mode*/
+ if (is_tablet) {
+ using namespace Inkscape::LivePathEffect;
+ SPItem * item = sp_event_context_find_item (desktop, Geom::Point(revent.x, revent.y), FALSE, FALSE);
+ if (item && (!this->white_item || item != white_item)) {
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ if (lpe) {
+ LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe);
+ if (ps) {
+ desktop->selection->clear();
+ desktop->selection->add(item);
+ }
+ }
+ }
+ }
+ break;
+ case SP_PENCIL_CONTEXT_ADDLINE:
+ /* Finish segment now */
+ if (anchor) {
+ p = anchor->dp;
+ } else {
+ this->_endpointSnap(p, revent.state);
+ }
+ this->ea = anchor;
+ this->_setEndpoint(p);
+ this->_finishEndpoint();
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ sp_event_context_discard_delayed_snap_event(this);
+ break;
+ case SP_PENCIL_CONTEXT_FREEHAND:
+ if (revent.state & GDK_MOD1_MASK && !tablet_enabled) {
+ /* sketch mode: interpolate the sketched path and improve the current output path with the new interpolation. don't finish sketch */
+ this->_sketchInterpolate();
+
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+
+ this->_state = SP_PENCIL_CONTEXT_SKETCH;
+ } else {
+ /* Finish segment now */
+ /// \todo fixme: Clean up what follows (Lauris)
+ if (anchor) {
+ p = anchor->dp;
+ } else {
+ Geom::Point p_end = p;
+ if (tablet_enabled) {
+ this->_addFreehandPoint(p_end, revent.state, true);
+ this->_pressure_curve->reset();
+ } else {
+ this->_endpointSnap(p_end, revent.state);
+ if (p_end != p) {
+ // then we must have snapped!
+ this->_addFreehandPoint(p_end, revent.state, true);
+ }
+ }
+ }
+
+ this->ea = anchor;
+ /* Write curves to object */
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand"));
+ this->_interpolate();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (tablet_enabled) {
+ gint shapetype = prefs->getInt("/tools/freehand/pencil/shape", 0);
+ gint simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0);
+ gint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ prefs->setInt("/tools/freehand/pencil/shape", 0);
+ prefs->setInt("/tools/freehand/pencil/simplify", 0);
+ prefs->setInt("/tools/freehand/pencil/freehand-mode", 0);
+ spdc_concat_colors_and_flush(this, FALSE);
+ prefs->setInt("/tools/freehand/pencil/freehand-mode", mode);
+ prefs->setInt("/tools/freehand/pencil/simplify", simplify);
+ prefs->setInt("/tools/freehand/pencil/shape", shapetype);
+ } else {
+ spdc_concat_colors_and_flush(this, FALSE);
+ }
+ this->points.clear();
+ this->sa = nullptr;
+ this->ea = nullptr;
+ this->ps.clear();
+ this->_wps.clear();
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ // reset sketch mode too
+ this->sketch_n = 0;
+ }
+ break;
+ case SP_PENCIL_CONTEXT_SKETCH:
+ default:
+ break;
+ }
+
+ if (this->grab) {
+ /* Release grab now */
+ sp_canvas_item_ungrab(this->grab);
+ this->grab = nullptr;
+ }
+
+ ret = true;
+ }
+ return ret;
+}
+
+void PencilTool::_cancel() {
+ if (this->grab) {
+ /* Release grab now */
+ sp_canvas_item_ungrab(this->grab);
+ this->grab = nullptr;
+ }
+
+ this->_is_drawing = false;
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ this->red_curve->reset();
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+ for (auto i:this->green_bpaths) {
+ sp_canvas_item_destroy(i);
+ }
+ this->green_bpaths.clear();
+ this->green_curve->reset();
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+
+ this->desktop->canvas->endForcedFullRedraws();
+}
+
+bool PencilTool::_handleKeyPress(GdkEventKey const &event) {
+ bool ret = false;
+
+ switch (get_latin_keyval(&event)) {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // Prevent the zoom field from activation.
+ if (!Inkscape::UI::held_only_control(event)) {
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Escape:
+ if (this->_npoints != 0) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ if (this->_state != SP_PENCIL_CONTEXT_IDLE) {
+ this->_cancel();
+ ret = true;
+ }
+ }
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (Inkscape::UI::held_only_control(event) && this->_npoints != 0) {
+ // if drawing, cancel, otherwise pass it up for undo
+ if (this->_state != SP_PENCIL_CONTEXT_IDLE) {
+ this->_cancel();
+ ret = true;
+ }
+ }
+ break;
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (Inkscape::UI::held_only_shift(event)) {
+ this->desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Meta_L:
+ case GDK_KEY_Meta_R:
+ if (this->_state == SP_PENCIL_CONTEXT_IDLE) {
+ this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("<b>Sketch mode</b>: holding <b>Alt</b> interpolates between sketched paths. Release <b>Alt</b> to finalize."));
+ }
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+bool PencilTool::_handleKeyRelease(GdkEventKey const &event) {
+ bool ret = false;
+
+ switch (get_latin_keyval(&event)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Meta_L:
+ case GDK_KEY_Meta_R:
+ if (this->_state == SP_PENCIL_CONTEXT_SKETCH) {
+ spdc_concat_colors_and_flush(this, FALSE);
+ this->sketch_n = 0;
+ this->sa = nullptr;
+ this->ea = nullptr;
+ if (this->green_anchor) {
+ this->green_anchor = sp_draw_anchor_destroy(this->green_anchor);
+ }
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ sp_event_context_discard_delayed_snap_event(this);
+ this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand sketch"));
+ ret = true;
+ }
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+/**
+ * Reset points and set new starting point.
+ */
+void PencilTool::_setStartpoint(Geom::Point const &p) {
+ this->_npoints = 0;
+ this->red_curve_is_valid = false;
+ if (in_svg_plane(p)) {
+ this->p[this->_npoints++] = p;
+ }
+}
+
+/**
+ * Change moving endpoint position.
+ * <ul>
+ * <li>Ctrl constrains to moving to H/V direction, snapping in given direction.
+ * <li>Otherwise we snap freely to whatever attractors are available.
+ * </ul>
+ *
+ * Number of points is (re)set to 2 always, 2nd point is modified.
+ * We change RED curve.
+ */
+void PencilTool::_setEndpoint(Geom::Point const &p) {
+ if (this->_npoints == 0) {
+ return;
+ /* May occur if first point wasn't in SVG plane (e.g. weird w2d transform, perhaps from bad
+ * zoom setting).
+ */
+ }
+ g_return_if_fail( this->_npoints > 0 );
+
+ this->red_curve->reset();
+ if ( ( p == this->p[0] )
+ || !in_svg_plane(p) )
+ {
+ this->_npoints = 1;
+ } else {
+ this->p[1] = p;
+ this->_npoints = 2;
+
+ this->red_curve->moveto(this->p[0]);
+ this->red_curve->lineto(this->p[1]);
+ this->red_curve_is_valid = true;
+ if (!tablet_enabled) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve);
+ }
+ }
+}
+
+/**
+ * Finalize addline.
+ *
+ * \todo
+ * fixme: I'd like remove red reset from concat colors (lauris).
+ * Still not sure, how it will make most sense.
+ */
+void PencilTool::_finishEndpoint() {
+ if (this->red_curve->is_unset() ||
+ this->red_curve->first_point() == this->red_curve->second_point())
+ {
+ this->red_curve->reset();
+ if (!tablet_enabled) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr);
+ }
+ } else {
+ /* Write curves to object. */
+ spdc_concat_colors_and_flush(this, FALSE);
+ this->sa = nullptr;
+ this->ea = nullptr;
+ }
+}
+
+static inline double square(double const x) { return x * x; }
+
+
+
+void PencilTool::addPowerStrokePencil()
+{
+ if (this->_curve) {
+ SPDocument *document = SP_ACTIVE_DOCUMENT;
+ if (!document) {
+ return;
+ }
+ using namespace Inkscape::LivePathEffect;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4;
+ double tolerance_sq = 0.02 * square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2);
+ int n_points = this->ps.size();
+ // worst case gives us a segment per point
+ int max_segs = 4 * n_points;
+ std::vector<Geom::Point> b(max_segs);
+ SPCurve *curvepressure = new SPCurve();
+ int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs);
+ if (n_segs > 0) {
+ /* Fit and draw and reset state */
+ curvepressure->moveto(b[0]);
+ for (int c = 0; c < n_segs; c++) {
+ curvepressure->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]);
+ }
+ }
+ Geom::Affine transform_coordinate = SP_ITEM(SP_ACTIVE_DESKTOP->currentLayer())->i2dt_affine().inverse();
+ curvepressure->transform(transform_coordinate);
+ Geom::Path path = curvepressure->get_pathvector()[0];
+ if (!path.empty()) {
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *pp = nullptr;
+ pp = xml_doc->createElement("svg:path");
+ gchar *pvector_str = sp_svg_write_path(path);
+ if (pvector_str) {
+ pp->setAttribute("d", pvector_str);
+ g_free(pvector_str);
+ }
+ pp->setAttribute("id", "power_stroke_preview");
+ Inkscape::GC::release(pp);
+
+ SPShape *powerpreview = SP_SHAPE(SP_ITEM(SP_ACTIVE_DESKTOP->currentLayer())->appendChildRepr(pp));
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(powerpreview);
+ if (!lpeitem) {
+ return;
+ }
+ Inkscape::DocumentUndo::ScopedInsensitive no_undo(document);
+ tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 0.0, 100.0) + 30;
+ if (tol > 30) {
+ tol = tol / (130.0 * (132.0 - tol));
+ Inkscape::SVGOStringStream threshold;
+ threshold << tol;
+ Effect::createAndApply(SIMPLIFY, desktop->doc(), SP_ITEM(lpeitem));
+ Effect *lpe = lpeitem->getCurrentLPE();
+ Inkscape::LivePathEffect::LPESimplify *simplify =
+ static_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe);
+ if (simplify) {
+ sp_lpe_item_enable_path_effects(lpeitem, false);
+ Glib::ustring pref_path = "/live_effects/simplify/smooth_angles";
+ bool valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ lpe->getRepr()->setAttribute("smooth_angles", "0");
+ }
+ pref_path = "/live_effects/simplify/helper_size";
+ valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ lpe->getRepr()->setAttribute("helper_size", "0");
+ }
+ pref_path = "/live_effects/simplify/step";
+ valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ lpe->getRepr()->setAttribute("step", "1");
+ }
+ lpe->getRepr()->setAttribute("threshold", threshold.str());
+ lpe->getRepr()->setAttribute("simplify_individual_paths", "false");
+ lpe->getRepr()->setAttribute("simplify_just_coalesce", "false");
+ sp_lpe_item_enable_path_effects(lpeitem, true);
+ }
+ sp_lpe_item_update_patheffect(lpeitem, false, true);
+ curvepressure = powerpreview->getCurve();
+ if (curvepressure->is_empty()) {
+ return;
+ }
+ path = curvepressure->get_pathvector()[0];
+ }
+ powerStrokeInterpolate(path);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil";
+ prefs->setBool(pref_path_pp, true);
+ Effect::createAndApply(POWERSTROKE, SP_ACTIVE_DESKTOP->doc(), lpeitem);
+ Effect *lpe = lpeitem->getCurrentLPE();
+ Inkscape::LivePathEffect::LPEPowerStroke *pspreview = static_cast<LPEPowerStroke *>(lpe);
+ if (pspreview) {
+ sp_lpe_item_enable_path_effects(lpeitem, false);
+ Glib::ustring pref_path = "/live_effects/powerstroke/interpolator_type";
+ bool valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ pspreview->getRepr()->setAttribute("interpolator_type", "CentripetalCatmullRom");
+ }
+ pref_path = "/live_effects/powerstroke/linejoin_type";
+ valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ pspreview->getRepr()->setAttribute("linejoin_type", "spiro");
+ }
+ pref_path = "/live_effects/powerstroke/interpolator_beta";
+ valid = prefs->getEntry(pref_path).isValid();
+ if (!valid) {
+ pspreview->getRepr()->setAttribute("interpolator_beta", "0.75");
+ }
+ gint cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2);
+ pspreview->getRepr()->setAttribute("start_linecap_type", LineCapTypeConverter.get_key(cap));
+ pspreview->getRepr()->setAttribute("end_linecap_type", LineCapTypeConverter.get_key(cap));
+ pspreview->getRepr()->setAttribute("sort_points", "true");
+ if (!this->points.size()) {
+ Geom::Point default_point((path.size()/2.0), 0.5);
+ this->points.push_back(default_point);
+ }
+ pspreview->offset_points.param_set_and_write_new_value(this->points);
+ sp_lpe_item_enable_path_effects(lpeitem, true);
+ sp_lpe_item_update_patheffect(lpeitem, false, true);
+ pp->setAttribute("style", "fill:#888888;opacity:1;fill-rule:nonzero;stroke:none;");
+ }
+ if (curvepressure) {
+ curvepressure->unref();
+ }
+ prefs->setBool(pref_path_pp, false);
+ }
+ }
+}
+
+/**
+ * Add a virtual point to the future pencil path.
+ *
+ * @param p the point to add.
+ * @param state event state
+ * @param last the point is the last of the user stroke.
+ */
+void PencilTool::_addFreehandPoint(Geom::Point const &p, guint /*state*/, bool last)
+{
+ g_assert( this->_npoints > 0 );
+ g_return_if_fail(unsigned(this->_npoints) < G_N_ELEMENTS(this->p));
+
+ double distance = 0;
+ if ( ( p != this->p[ this->_npoints - 1 ] )
+ && in_svg_plane(p) )
+ {
+ this->p[this->_npoints++] = p;
+ this->_fitAndSplit();
+ if (tablet_enabled) {
+ distance = Geom::distance(p, this->ps.back()) + this->_wps.back()[Geom::X];
+ }
+ this->ps.push_back(p);
+ }
+ if (tablet_enabled && in_svg_plane(p)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double min = prefs->getIntLimited("/tools/freehand/pencil/minpressure", 0, 0, 100) / 100.0;
+ double max = prefs->getIntLimited("/tools/freehand/pencil/maxpressure", 30, 0, 100) / 100.0;
+ if (min > max) {
+ min = max;
+ }
+ double dezoomify_factor = 0.05 * 1000 / SP_EVENT_CONTEXT(this)->desktop->current_zoom();
+ double pressure_shrunk = (((this->pressure - 0.25) * 1.25) * (max - min)) + min;
+ double pressure_computed = pressure_shrunk * dezoomify_factor;
+ double pressure_computed_scaled = std::abs(pressure_computed * SP_ACTIVE_DOCUMENT->getDocumentScale().inverse()[Geom::X]);
+ if (p != this->p[this->_npoints - 1]) {
+ this->_wps.emplace_back(distance, pressure_computed_scaled);
+ }
+ pressure_computed = std::abs(pressure_computed);
+ if (pressure_computed) {
+ Geom::Circle pressure_dot(p, pressure_computed);
+ Geom::Piecewise<Geom::D2<Geom::SBasis>> pressure_piecewise;
+ pressure_piecewise.push_cut(0);
+ pressure_piecewise.push(pressure_dot.toSBasis(), 1);
+ Geom::PathVector pressure_path = Geom::path_from_piecewise(pressure_piecewise, 0.1);
+ Geom::PathVector previous_presure = this->_pressure_curve->get_pathvector();
+ if (!pressure_path.empty() && !previous_presure.empty()) {
+ pressure_path = sp_pathvector_boolop(pressure_path, previous_presure, bool_op_union, fill_nonZero, fill_nonZero);
+ }
+ this->_pressure_curve->set_pathvector(pressure_path);
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->_pressure_curve);
+ }
+ if (last) {
+ this->addPowerStrokePencil();
+ }
+ }
+}
+
+void PencilTool::powerStrokeInterpolate(Geom::Path const path)
+{
+ size_t ps_size = this->ps.size();
+ if ( ps_size <= 1 ) {
+ return;
+ }
+
+ using Geom::X;
+ using Geom::Y;
+ gint path_size = path.size();
+ std::vector<Geom::Point> tmp_points;
+ Geom::Point previous = Geom::Point(Geom::infinity(), 0);
+ bool increase = false;
+ size_t i = 0;
+ double dezoomify_factor = 0.05 * 1000 / SP_EVENT_CONTEXT(this)->desktop->current_zoom();
+ double limit = 6 * dezoomify_factor;
+ double max =
+ std::max(this->_wps.back()[Geom::X] - (this->_wps.back()[Geom::X] / 10), this->_wps.back()[Geom::X] - limit);
+ double min = std::min(this->_wps.back()[Geom::X] / 10, limit);
+ double original_lenght = this->_wps.back()[Geom::X];
+ double max10 = 0;
+ double min10 = 0;
+ for (auto wps : this->_wps) {
+ i++;
+ Geom::Coord pressure = wps[Geom::Y];
+ max10 = max10 > pressure ? max10 : pressure;
+ min10 = min10 <= pressure ? min10 : pressure;
+
+ if (!original_lenght || wps[Geom::X] > max) {
+ break;
+ }
+ if (wps[Geom::Y] == 0 || wps[Geom::X] < min) {
+ continue;
+ }
+ if (previous[Geom::Y] < (max10 + min10) / 2.0) {
+ if (increase && tmp_points.size() > 1) {
+ tmp_points.pop_back();
+ }
+ wps[Geom::Y] = max10;
+ tmp_points.push_back(wps);
+ increase = true;
+ } else {
+ if (!increase && tmp_points.size() > 1) {
+ tmp_points.pop_back();
+ }
+ wps[Geom::Y] = min10;
+ tmp_points.push_back(wps);
+ increase = false;
+ }
+ previous = wps;
+ max10 = 0;
+ min10 = 999999999;
+ }
+ this->points.clear();
+ double prev_pressure = 0;
+ for (auto point : tmp_points) {
+ point[Geom::X] /= (double)original_lenght;
+ point[Geom::X] *= path_size;
+ if (std::abs(point[Geom::Y] - prev_pressure) > point[Geom::Y] / 10.0) {
+ this->points.push_back(point);
+ prev_pressure = point[Geom::Y];
+ }
+ }
+ tmp_points.clear();
+}
+
+void PencilTool::_interpolate() {
+ size_t ps_size = this->ps.size();
+ if ( ps_size <= 1 ) {
+ return;
+ }
+ using Geom::X;
+ using Geom::Y;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4;
+ bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0);
+ if(simplify){
+ double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4;
+ tol = std::min(tol,tol2);
+ }
+ this->green_curve->reset();
+ this->red_curve->reset();
+ this->red_curve_is_valid = false;
+ double tolerance_sq = square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2);
+
+ g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent));
+
+ int n_points = this->ps.size();
+
+ // worst case gives us a segment per point
+ int max_segs = 4 * n_points;
+
+ std::vector<Geom::Point> b(max_segs);
+ int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs);
+ if (n_segs > 0) {
+ /* Fit and draw and reset state */
+ this->green_curve->moveto(b[0]);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ for (int c = 0; c < n_segs; c++) {
+ // if we are in BSpline we modify the trace to create adhoc nodes
+ if (mode == 2) {
+ Geom::Point point_at1 = b[4 * c + 0] + (1./3) * (b[4 * c + 3] - b[4 * c + 0]);
+ point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP);
+ Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]);
+ point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP);
+ this->green_curve->curveto(point_at1,point_at2,b[4*c+3]);
+ } else {
+ if (!tablet_enabled || c != n_segs - 1) {
+ this->green_curve->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]);
+ } else {
+ boost::optional<Geom::Point> finalp = this->green_curve->last_point();
+ if (this->green_curve->nodes_in_path() > 4 && Geom::are_near(*finalp, b[4 * c + 3], 10.0)) {
+ this->green_curve->backspace();
+ this->green_curve->curveto(*finalp, b[4 * c + 3], b[4 * c + 3]);
+ } else {
+ this->green_curve->curveto(b[4 * c + 1], b[4 * c + 3], b[4 * c + 3]);
+ }
+ }
+ }
+ }
+ if (!tablet_enabled) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->green_curve);
+ }
+ /* Fit and draw and copy last point */
+ g_assert(!this->green_curve->is_empty());
+ /* Set up direction of next curve. */
+ {
+ Geom::Curve const * last_seg = this->green_curve->last_segment();
+ g_assert( last_seg ); // Relevance: validity of (*last_seg)
+ this->p[0] = last_seg->finalPoint();
+ this->_npoints = 1;
+ Geom::Curve *last_seg_reverse = last_seg->reverse();
+ Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) );
+ delete last_seg_reverse;
+ this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) )
+ ? Geom::Point(0, 0)
+ : Geom::unit_vector(req_vec) );
+ }
+ }
+}
+
+
+/* interpolates the sketched curve and tweaks the current sketch interpolation*/
+void PencilTool::_sketchInterpolate() {
+ if ( this->ps.size() <= 1 ) {
+ return;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4;
+ bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0);
+ if(simplify){
+ double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 1.0, 100.0) * 0.4;
+ tol = std::min(tol,tol2);
+ }
+ double tolerance_sq = 0.02 * square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2);
+
+ bool average_all_sketches = prefs->getBool("/tools/freehand/pencil/average_all_sketches", true);
+
+ g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent));
+
+ this->red_curve->reset();
+ this->red_curve_is_valid = false;
+
+ int n_points = this->ps.size();
+
+ // worst case gives us a segment per point
+ int max_segs = 4 * n_points;
+
+ std::vector<Geom::Point> b(max_segs);
+
+ int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs);
+
+ if (n_segs > 0) {
+ Geom::Path fit(b[0]);
+
+ for (int c = 0; c < n_segs; c++) {
+ fit.appendNew<Geom::CubicBezier>(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]);
+ }
+
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > fit_pwd2 = fit.toPwSb();
+
+ if (this->sketch_n > 0) {
+ double t;
+
+ if (average_all_sketches) {
+ // Average = (sum of all) / n
+ // = (sum of all + new one) / n+1
+ // = ((old average)*n + new one) / n+1
+ t = this->sketch_n / (this->sketch_n + 1.);
+ } else {
+ t = 0.5;
+ }
+
+ this->sketch_interpolation = Geom::lerp(t, fit_pwd2, this->sketch_interpolation);
+
+ // simplify path, to eliminate small segments
+ Path path;
+ path.LoadPathVector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01));
+ path.Simplify(0.5);
+
+ Geom::PathVector *pathv = path.MakePathVector();
+ this->sketch_interpolation = (*pathv)[0].toPwSb();
+ delete pathv;
+ } else {
+ this->sketch_interpolation = fit_pwd2;
+ }
+
+ this->sketch_n++;
+
+ this->green_curve->reset();
+ this->green_curve->set_pathvector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01));
+ if (!tablet_enabled) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->green_curve);
+ }
+ /* Fit and draw and copy last point */
+ g_assert(!this->green_curve->is_empty());
+
+ /* Set up direction of next curve. */
+ {
+ Geom::Curve const * last_seg = this->green_curve->last_segment();
+ g_assert( last_seg ); // Relevance: validity of (*last_seg)
+ this->p[0] = last_seg->finalPoint();
+ this->_npoints = 1;
+ Geom::Curve *last_seg_reverse = last_seg->reverse();
+ Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) );
+ delete last_seg_reverse;
+ this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) )
+ ? Geom::Point(0, 0)
+ : Geom::unit_vector(req_vec) );
+ }
+ }
+
+ this->ps.clear();
+ this->points.clear();
+ this->_wps.clear();
+}
+
+void PencilTool::_fitAndSplit() {
+ g_assert( this->_npoints > 1 );
+
+ double const tolerance_sq = 0;
+
+ Geom::Point b[4];
+ g_assert(is_zero(this->_req_tangent)
+ || is_unit_vector(this->_req_tangent));
+ Geom::Point const tHatEnd(0, 0);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const n_segs = Geom::bezier_fit_cubic_full(b, nullptr, this->p, this->_npoints,
+ this->_req_tangent, tHatEnd,
+ tolerance_sq, 1);
+ if ( n_segs > 0
+ && unsigned(this->_npoints) < G_N_ELEMENTS(this->p) )
+ {
+ /* Fit and draw and reset state */
+
+ this->red_curve->reset();
+ this->red_curve->moveto(b[0]);
+ using Geom::X;
+ using Geom::Y;
+ // if we are in BSpline we modify the trace to create adhoc nodes
+ guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ if(mode == 2){
+ Geom::Point point_at1 = b[0] + (1./3)*(b[3] - b[0]);
+ point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP);
+ Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]);
+ point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP);
+ this->red_curve->curveto(point_at1,point_at2,b[3]);
+ }else{
+ this->red_curve->curveto(b[1], b[2], b[3]);
+ }
+ if (!tablet_enabled) {
+ sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve);
+ }
+ this->red_curve_is_valid = true;
+ } else {
+ /* Fit and draw and copy last point */
+
+ g_assert(!this->red_curve->is_empty());
+
+ /* Set up direction of next curve. */
+ {
+ Geom::Curve const * last_seg = this->red_curve->last_segment();
+ g_assert( last_seg ); // Relevance: validity of (*last_seg)
+ this->p[0] = last_seg->finalPoint();
+ this->_npoints = 1;
+ Geom::Curve *last_seg_reverse = last_seg->reverse();
+ Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) );
+ delete last_seg_reverse;
+ this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) )
+ ? Geom::Point(0, 0)
+ : Geom::unit_vector(req_vec) );
+ }
+
+
+ this->green_curve->append_continuous(this->red_curve, 0.0625);
+ SPCurve *curve = this->red_curve->copy();
+
+ /// \todo fixme:
+ SPCanvasItem *cshape = sp_canvas_bpath_new(this->desktop->getSketch(), curve, true);
+ curve->unref();
+
+ this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color();
+ if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){
+ this->green_color = 0x00ff007f;
+ } else {
+ this->green_color = this->highlight_color;
+ }
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cshape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+
+ this->green_bpaths.push_back(cshape);
+
+ this->red_curve_is_valid = 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/tools/pencil-tool.h b/src/ui/tools/pencil-tool.h
new file mode 100644
index 0000000..41cb71a
--- /dev/null
+++ b/src/ui/tools/pencil-tool.h
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * PencilTool: a context for pencil tool events
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_PENCIL_CONTEXT_H
+#define SEEN_PENCIL_CONTEXT_H
+
+
+#include "ui/tools/freehand-base.h"
+
+#include <2geom/piecewise.h>
+#include <2geom/d2.h>
+#include <2geom/sbasis.h>
+#include <2geom/pathvector.h>
+// #include <future>
+
+class SPShape;
+
+#define DDC_MIN_PRESSURE 0.0
+#define DDC_MAX_PRESSURE 1.0
+#define DDC_DEFAULT_PRESSURE 1.0
+#define SP_PENCIL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PencilTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_PENCIL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PencilTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum PencilState {
+ SP_PENCIL_CONTEXT_IDLE,
+ SP_PENCIL_CONTEXT_ADDLINE,
+ SP_PENCIL_CONTEXT_FREEHAND,
+ SP_PENCIL_CONTEXT_SKETCH
+};
+
+/**
+ * PencilTool: a context for pencil tool events
+ */
+class PencilTool : public FreehandBase {
+public:
+ PencilTool();
+ ~PencilTool() override;
+ Geom::Point p[16];
+ std::vector<Geom::Point> ps;
+ std::vector<Geom::Point> points;
+ void addPowerStrokePencil();
+ void powerStrokeInterpolate(Geom::Path const path);
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > sketch_interpolation; // the current proposal from the sketched paths
+ unsigned sketch_n; // number of sketches done
+ static const std::string prefsPath;
+ const std::string& getPrefsPath() override;
+
+protected:
+
+ void setup() override;
+ bool root_handler(GdkEvent* event) override;
+
+private:
+ bool _handleButtonPress(GdkEventButton const &bevent);
+ bool _handleMotionNotify(GdkEventMotion const &mevent);
+ bool _handleButtonRelease(GdkEventButton const &revent);
+ bool _handleKeyPress(GdkEventKey const &event);
+ bool _handleKeyRelease(GdkEventKey const &event);
+ void _setStartpoint(Geom::Point const &p);
+ void _setEndpoint(Geom::Point const &p);
+ void _finishEndpoint();
+ void _addFreehandPoint(Geom::Point const &p, guint state, bool last);
+ void _fitAndSplit();
+ void _interpolate();
+ void _sketchInterpolate();
+ void _extinput(GdkEvent *event);
+ void _cancel();
+ void _endpointSnap(Geom::Point &p, guint const state);
+ std::vector<Geom::Point> _wps;
+ SPCurve * _curve;
+ SPCurve *_pressure_curve;
+ Geom::Point _req_tangent;
+ bool _is_drawing;
+ PencilState _state;
+ gint _npoints;
+ // std::future<bool> future;
+};
+
+}
+}
+}
+
+#endif /* !SEEN_PENCIL_CONTEXT_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/tools/rect-tool.cpp b/src/ui/tools/rect-tool.cpp
new file mode 100644
index 0000000..2cd10a2
--- /dev/null
+++ b/src/ui/tools/rect-tool.cpp
@@ -0,0 +1,503 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Rectangle drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000-2005 authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "display/sp-canvas-item.h"
+#include "display/sp-canvas.h"
+
+#include "object/sp-rect.h"
+#include "object/sp-namedview.h"
+
+#include "ui/pixmaps/cursor-rect.xpm"
+
+#include "ui/shape-editor.h"
+#include "ui/tools/rect-tool.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& RectTool::getPrefsPath() {
+ return RectTool::prefsPath;
+}
+
+const std::string RectTool::prefsPath = "/tools/shapes/rect";
+
+RectTool::RectTool()
+ : ToolBase(cursor_rect_xpm)
+ , rect(nullptr)
+ , rx(0)
+ , ry(0)
+{
+}
+
+void RectTool::finish() {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate));
+
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ ToolBase::finish();
+}
+
+RectTool::~RectTool() {
+ this->enableGrDrag(false);
+
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->rect) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ */
+void RectTool::selection_changed(Inkscape::Selection* selection) {
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+}
+
+void RectTool::setup() {
+ ToolBase::setup();
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = this->desktop->getSelection()->connectChanged(
+ sigc::mem_fun(this, &RectTool::selection_changed)
+ );
+
+ sp_event_context_read(this, "rx");
+ sp_event_context_read(this, "ry");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void RectTool::set(const Inkscape::Preferences::Entry& val) {
+ /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like
+ * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */
+ Glib::ustring name = val.getEntryName();
+
+ if ( name == "rx" ) {
+ this->rx = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up
+ } else if ( name == "ry" ) {
+ this->ry = val.getDoubleLimited();
+ }
+}
+
+bool RectTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1 && !this->space_panning) {
+ Inkscape::setup_for_drag_start(desktop, this, event);
+ }
+ break;
+ // motion and release are always on root (why?)
+ default:
+ break;
+ }
+
+ ret = ToolBase::item_handler(item, event);
+
+ return ret;
+}
+
+bool RectTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ SPDesktop *desktop = this->desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ Geom::Point const button_w(event->button.x, event->button.y);
+
+ // save drag origin
+ this->xp = (gint) button_w[Geom::X];
+ this->yp = (gint) button_w[Geom::Y];
+ this->within_tolerance = true;
+
+ // remember clicked item, disregarding groups, honoring Alt
+ this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+
+ dragging = true;
+
+ /* Position center */
+ Geom::Point button_dt(desktop->w2d(button_w));
+ this->center = button_dt;
+
+ /* Snap center */
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ this->center = button_dt;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ ( GDK_KEY_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_POINTER_MOTION_HINT_MASK |
+ GDK_BUTTON_PRESS_MASK ),
+ nullptr, event->button.time);
+
+ ret = TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if ( dragging
+ && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning)
+ {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ this->drag(motion_dt, event->motion.state); // this will also handle the snapping
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ ret = TRUE;
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the rect
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ }
+ break;
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+ if (!dragging){
+ sp_event_show_modifier_tip (this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: make square or integer-ratio rect, lock a rounded corner circular"),
+ _("<b>Shift</b>: draw around the starting point"),
+ nullptr);
+ }
+ break;
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo("rect-width");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (dragging) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the rect
+ this->finishItem();
+ }
+ // do not return true, so that space would work switching to selector
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void RectTool::drag(Geom::Point const pt, guint state) {
+ SPDesktop *desktop = this->desktop;
+
+ if (!this->rect) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return;
+ }
+
+ // Create object
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:rect");
+
+ // Set style
+ sp_desktop_apply_style_tool (desktop, repr, "/tools/shapes/rect", false);
+
+ this->rect = SP_RECT(desktop->currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+
+ this->rect->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ this->rect->updateRepr();
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ }
+
+ Geom::Rect const r = Inkscape::snap_rectangular_box(desktop, this->rect, pt, this->center, state);
+
+ this->rect->setPosition(r.min()[Geom::X], r.min()[Geom::Y], r.dimensions()[Geom::X], r.dimensions()[Geom::Y]);
+
+ if (this->rx != 0.0) {
+ this->rect->setRx(true, this->rx);
+ }
+
+ if (this->ry != 0.0) {
+ if (this->rx == 0.0)
+ this->rect->setRy(true, CLAMP(this->ry, 0, MIN(r.dimensions()[Geom::X], r.dimensions()[Geom::Y])/2));
+ else
+ this->rect->setRy(true, CLAMP(this->ry, 0, r.dimensions()[Geom::Y]));
+ }
+
+ // status text
+ double rdimx = r.dimensions()[Geom::X];
+ double rdimy = r.dimensions()[Geom::Y];
+
+ Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px");
+ Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px");
+ Glib::ustring xs = rdimx_q.string(desktop->namedview->display_units);
+ Glib::ustring ys = rdimy_q.string(desktop->namedview->display_units);
+
+ if (state & GDK_CONTROL_MASK) {
+ int ratio_x, ratio_y;
+ bool is_golden_ratio = false;
+
+ if (fabs (rdimx) > fabs (rdimy)) {
+ if (fabs(rdimx / rdimy - goldenratio) < 1e-6) {
+ is_golden_ratio = true;
+ }
+
+ ratio_x = (int) rint (rdimx / rdimy);
+ ratio_y = 1;
+ } else {
+ if (fabs(rdimy / rdimx - goldenratio) < 1e-6) {
+ is_golden_ratio = true;
+ }
+
+ ratio_x = 1;
+ ratio_y = (int) rint (rdimy / rdimx);
+ }
+
+ if (!is_golden_ratio) {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Rectangle</b>: %s &#215; %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str(), ratio_x, ratio_y);
+ } else {
+ if (ratio_y == 1) {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Rectangle</b>: %s &#215; %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str());
+ } else {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Rectangle</b>: %s &#215; %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str());
+ }
+ }
+ } else {
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Rectangle</b>: %s &#215; %s; with <b>Ctrl</b> to make square, integer-ratio, or golden-ratio rectangle; with <b>Shift</b> to draw around the starting point"),
+ xs.c_str(), ys.c_str());
+ }
+}
+
+void RectTool::finishItem() {
+ this->message_context->clear();
+
+ if (this->rect != nullptr) {
+ if (this->rect->width.computed == 0 || this->rect->height.computed == 0) {
+ this->cancel(); // Don't allow the creating of zero sized rectangle, for example when the start and and point snap to the snap grid point
+ return;
+ }
+
+ this->rect->updateRepr();
+ this->rect->doWriteTransform(this->rect->transform, nullptr, true);
+
+ this->desktop->canvas->endForcedFullRedraws();
+
+ this->desktop->getSelection()->set(this->rect);
+
+ DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_RECT, _("Create rectangle"));
+
+ this->rect = nullptr;
+ }
+}
+
+void RectTool::cancel(){
+ this->desktop->getSelection()->clear();
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate));
+
+ if (this->rect != nullptr) {
+ this->rect->deleteObject();
+ this->rect = nullptr;
+ }
+
+ this->within_tolerance = false;
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+
+ this->desktop->canvas->endForcedFullRedraws();
+
+ DocumentUndo::cancel(this->desktop->getDocument());
+}
+
+}
+}
+}
+
+/*
+ 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/tools/rect-tool.h b/src/ui/tools/rect-tool.h
new file mode 100644
index 0000000..703b7c2
--- /dev/null
+++ b/src/ui/tools/rect-tool.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_RECT_CONTEXT_H__
+#define __SP_RECT_CONTEXT_H__
+
+/*
+ * Rectangle drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+
+class SPRect;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class RectTool : public ToolBase {
+public:
+ RectTool();
+ ~RectTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPRect *rect;
+ Geom::Point center;
+
+ gdouble rx; /* roundness radius (x direction) */
+ gdouble ry; /* roundness radius (y direction) */
+
+ sigc::connection sel_changed_connection;
+
+ void drag(Geom::Point const pt, guint state);
+ void finishItem();
+ void cancel();
+ void selection_changed(Inkscape::Selection* selection);
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp
new file mode 100644
index 0000000..c3a7b63
--- /dev/null
+++ b/src/ui/tools/select-tool.cpp
@@ -0,0 +1,1171 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Selection and transformation context
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 authors
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 1999-2005 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 <cstring>
+#include <string>
+
+#include <gtkmm/widget.h>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "include/macros.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection-describer.h"
+#include "selection.h"
+#include "seltrans.h"
+#include "sp-cursor.h"
+
+#include "display/drawing-item.h"
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-item.h"
+
+#include "object/box3d.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-select-d.xpm"
+#include "ui/pixmaps/cursor-select-m.xpm"
+#include "ui/pixmaps/handles.xpm"
+
+#include "ui/tools-switch.h"
+#include "ui/tools/select-tool.h"
+
+#ifdef WITH_DBUS
+#include "extension/dbus/document-interface.h"
+#endif
+
+
+using Inkscape::DocumentUndo;
+
+GdkPixbuf *handles[23];
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static GdkCursor *CursorSelectMouseover = nullptr;
+static GdkCursor *CursorSelectDragging = nullptr;
+
+static gint rb_escaped = 0; // if non-zero, rubberband was canceled by esc, so the next button release should not deselect
+static gint drag_escaped = 0; // if non-zero, drag was canceled by esc
+
+const std::string& SelectTool::getPrefsPath() {
+ return SelectTool::prefsPath;
+}
+
+const std::string SelectTool::prefsPath = "/tools/select";
+
+
+//Creates rotated variations for handles
+static void
+sp_load_handles(int start, int count, char const **xpm) {
+ handles[start] = gdk_pixbuf_new_from_xpm_data((gchar const **)xpm);
+ for(int i = start + 1; i < start + count; i++) {
+ // We use either the original at *start or previous loop item to rotate
+ handles[i] = gdk_pixbuf_rotate_simple(handles[i-1], GDK_PIXBUF_ROTATE_CLOCKWISE);
+ }
+}
+
+SelectTool::SelectTool()
+ // Don't load a default cursor
+ : ToolBase(nullptr)
+ , dragging(false)
+ , moved(false)
+ , button_press_shift(false)
+ , button_press_ctrl(false)
+ , button_press_alt(false)
+ , cycling_wrap(true)
+ , item(nullptr)
+ , grabbed(nullptr)
+ , _seltrans(nullptr)
+ , _describer(nullptr)
+{
+ // cursors in select context
+ CursorSelectMouseover = sp_cursor_from_xpm(cursor_select_m_xpm);
+ CursorSelectDragging = sp_cursor_from_xpm(cursor_select_d_xpm);
+
+ // selection handles
+ sp_load_handles(0, 2, handle_scale_xpm);
+ sp_load_handles(2, 2, handle_stretch_xpm);
+ sp_load_handles(4, 4, handle_rotate_xpm);
+ sp_load_handles(8, 4, handle_skew_xpm);
+ sp_load_handles(12, 1, handle_center_xpm);
+ sp_load_handles(13, 4, handle_align_xpm);
+ sp_load_handles(17, 1, handle_align_center_xpm);
+ sp_load_handles(18, 4, handle_align_corner_xpm);
+}
+
+//static gint xp = 0, yp = 0; // where drag started
+//static gint tolerance = 0;
+//static bool within_tolerance = false;
+static bool is_cycling = false;
+
+
+SelectTool::~SelectTool() {
+ this->enableGrDrag(false);
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ delete this->_seltrans;
+ this->_seltrans = nullptr;
+
+ delete this->_describer;
+ this->_describer = nullptr;
+
+ if (CursorSelectDragging) {
+ g_object_unref(CursorSelectDragging);
+ CursorSelectDragging = nullptr;
+ }
+
+ if (CursorSelectMouseover) {
+ g_object_unref(CursorSelectMouseover);
+ CursorSelectMouseover = nullptr;
+ }
+ this->desktop->canvas->endForcedFullRedraws();
+}
+
+void SelectTool::setup() {
+ ToolBase::setup();
+
+ this->_describer = new Inkscape::SelectionDescriber(
+ desktop->selection,
+ desktop->messageStack(),
+ _("Click selection to toggle scale/rotation handles (or Shift+s)"),
+ _("No objects selected. Click, Shift+click, Alt+scroll mouse on top of objects, or drag around objects to select.")
+ );
+
+ this->_seltrans = new Inkscape::SelTrans(desktop);
+
+ sp_event_context_read(this, "show");
+ sp_event_context_read(this, "transform");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/select/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void SelectTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "show") {
+ if (val.getString() == "outline") {
+ this->_seltrans->setShow(Inkscape::SelTrans::SHOW_OUTLINE);
+ } else {
+ this->_seltrans->setShow(Inkscape::SelTrans::SHOW_CONTENT);
+ }
+ }
+}
+
+bool SelectTool::sp_select_context_abort() {
+ Inkscape::SelTrans *seltrans = this->_seltrans;
+
+ if (this->dragging) {
+ if (this->moved) { // cancel dragging an object
+ seltrans->ungrab();
+ this->moved = FALSE;
+ this->dragging = FALSE;
+ sp_event_context_discard_delayed_snap_event(this);
+ drag_escaped = 1;
+
+ if (this->item) {
+ // only undo if the item is still valid
+ if (this->item->document) {
+ DocumentUndo::undo(desktop->getDocument());
+ }
+
+ sp_object_unref( this->item, nullptr);
+ } else if (this->button_press_ctrl) {
+ // NOTE: This is a workaround to a bug.
+ // When the ctrl key is held, sc->item is not defined
+ // so in this case (only), we skip the object doc check
+ DocumentUndo::undo(desktop->getDocument());
+ }
+ this->item = nullptr;
+
+ SP_EVENT_CONTEXT(this)->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Move canceled."));
+ return true;
+ }
+ } else {
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->stop();
+ rb_escaped = 1;
+ SP_EVENT_CONTEXT(this)->defaultMessageContext()->clear();
+ SP_EVENT_CONTEXT(this)->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selection canceled."));
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool
+key_is_a_modifier (guint key) {
+ return (key == GDK_KEY_Alt_L ||
+ key == GDK_KEY_Alt_R ||
+ key == GDK_KEY_Control_L ||
+ key == GDK_KEY_Control_R ||
+ key == GDK_KEY_Shift_L ||
+ key == GDK_KEY_Shift_R ||
+ key == GDK_KEY_Meta_L || // Meta is when you press Shift+Alt (at least on my machine)
+ key == GDK_KEY_Meta_R);
+}
+
+static void
+sp_select_context_up_one_layer(SPDesktop *desktop)
+{
+ /* Click in empty place, go up one level -- but don't leave a layer to root.
+ *
+ * (Rationale: we don't usually allow users to go to the root, since that
+ * detracts from the layer metaphor: objects at the root level can in front
+ * of or behind layers. Whereas it's fine to go to the root if editing
+ * a document that has no layers (e.g. a non-Inkscape document).)
+ *
+ * Once we support editing SVG "islands" (e.g. <svg> embedded in an xhtml
+ * document), we might consider further restricting the below to disallow
+ * leaving a layer to go to a non-layer.
+ */
+ SPObject *const current_layer = desktop->currentLayer();
+ if (current_layer) {
+ SPObject *const parent = current_layer->parent;
+ SPGroup *current_group = dynamic_cast<SPGroup *>(current_layer);
+ if ( parent
+ && ( parent->parent
+ || !( current_group
+ && ( SPGroup::LAYER == current_group->layerMode() ) ) ) )
+ {
+ desktop->setCurrentLayer(parent);
+ if (current_group && (SPGroup::LAYER != current_group->layerMode())) {
+ desktop->getSelection()->set(current_layer);
+ }
+ }
+ }
+}
+
+bool SelectTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ // make sure we still have valid objects to move around
+ if (this->item && this->item->document == nullptr) {
+ this->sp_select_context_abort();
+ }
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ /* Left mousebutton */
+
+ // save drag origin
+ xp = (gint) event->button.x;
+ yp = (gint) event->button.y;
+ within_tolerance = true;
+
+ // remember what modifiers were on before button press
+ this->button_press_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false;
+ this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false;
+ this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false;
+
+ if (event->button.state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) {
+ // if shift or ctrl was pressed, do not move objects;
+ // pass the event to root handler which will perform rubberband, shift-click, ctrl-click, ctrl-drag
+ } else {
+ GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+
+ this->dragging = TRUE;
+ this->moved = FALSE;
+
+ gdk_window_set_cursor(window, CursorSelectDragging);
+
+
+ // remember the clicked item in this->item:
+ if (this->item) {
+ sp_object_unref(this->item, nullptr);
+ this->item = nullptr;
+ }
+
+ this->item = sp_event_context_find_item (desktop,
+ Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE);
+ sp_object_ref(this->item, nullptr);
+
+ rb_escaped = drag_escaped = 0;
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->drawing),
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK,
+ nullptr, event->button.time);
+
+ this->grabbed = SP_CANVAS_ITEM(desktop->drawing);
+
+ ret = TRUE;
+ }
+ } else if (event->button.button == 3 && !this->dragging) {
+ // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband
+ this->sp_select_context_abort();
+ }
+ break;
+
+
+ case GDK_ENTER_NOTIFY: {
+ if (!desktop->isWaitingCursor() && !this->dragging) {
+ GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+
+ gdk_window_set_cursor(window, CursorSelectMouseover);
+ }
+ break;
+ }
+ case GDK_LEAVE_NOTIFY:
+ if (!desktop->isWaitingCursor() && !this->dragging) {
+ Glib::RefPtr<Gdk::Window> window = Glib::wrap(GTK_WIDGET(desktop->getCanvas()))->get_window();
+
+ window->set_cursor(this->cursor);
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ if (get_latin_keyval (&event->key) == GDK_KEY_space) {
+ if (this->dragging && this->grabbed) {
+ /* stamping mode: show content mode moving */
+ _seltrans->stamp();
+ ret = TRUE;
+ }
+ } else if (get_latin_keyval (&event->key) == GDK_KEY_Tab) {
+ if (this->dragging && this->grabbed) {
+ _seltrans->getNextClosestPoint(false);
+ ret = TRUE;
+ }
+ } else if (get_latin_keyval (&event->key) == GDK_KEY_ISO_Left_Tab) {
+ if (this->dragging && this->grabbed) {
+ _seltrans->getNextClosestPoint(true);
+ ret = TRUE;
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::item_handler(item, event);
+ }
+
+ return ret;
+}
+
+void SelectTool::sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event, bool shift_pressed) {
+ if ( this->cycling_items.empty() )
+ return;
+
+ Inkscape::DrawingItem *arenaitem;
+
+ if(cycling_cur_item) {
+ arenaitem = cycling_cur_item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(0.3);
+ }
+
+ // Find next item and activate it
+
+
+ std::vector<SPItem *>::iterator next = cycling_items.end();
+
+ if ((scroll_event->direction == GDK_SCROLL_UP) ||
+ (scroll_event->direction == GDK_SCROLL_SMOOTH && scroll_event->delta_y < 0)) {
+ if (! cycling_cur_item) {
+ next = cycling_items.begin();
+ } else {
+ next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item );
+ g_assert (next != cycling_items.end());
+ next++;
+ if (next == cycling_items.end()) {
+ if ( cycling_wrap ) {
+ next = cycling_items.begin();
+ } else {
+ next--;
+ }
+ }
+ }
+ } else {
+ if (! cycling_cur_item) {
+ next = cycling_items.end();
+ next--;
+ } else {
+ next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item );
+ g_assert (next != cycling_items.end());
+ if (next == cycling_items.begin()){
+ if ( cycling_wrap ) {
+ next = cycling_items.end();
+ next--;
+ }
+ } else {
+ next--;
+ }
+ }
+ }
+
+ this->cycling_cur_item = *next;
+ g_assert(next != cycling_items.end());
+ g_assert(cycling_cur_item != nullptr);
+
+ arenaitem = cycling_cur_item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(1.0);
+
+ if (shift_pressed) {
+ selection->add(cycling_cur_item);
+ } else {
+ selection->set(cycling_cur_item);
+ }
+}
+
+void SelectTool::sp_select_context_reset_opacities() {
+ for (auto item : this->cycling_items_cmp) {
+ if (item) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value));
+ } else {
+ g_assert_not_reached();
+ }
+ }
+
+ this->cycling_items_cmp.clear();
+ this->cycling_cur_item = nullptr;
+}
+
+bool SelectTool::root_handler(GdkEvent* event) {
+ SPItem *item = nullptr;
+ SPItem *item_at_point = nullptr, *group_at_point = nullptr, *item_in_group = nullptr;
+ gint ret = FALSE;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // make sure we still have valid objects to move around
+ if (this->item && this->item->document == nullptr) {
+ this->sp_select_context_abort();
+ }
+ desktop->canvas->forceFullRedrawAfterInterruptions(5, false);
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (event->button.button == 1) {
+ if (!selection->isEmpty()) {
+ SPItem *clicked_item = selection->items().front();
+
+ if (dynamic_cast<SPGroup *>(clicked_item) && !dynamic_cast<SPBox3D *>(clicked_item)) { // enter group if it's not a 3D box
+ desktop->setCurrentLayer(clicked_item);
+ desktop->getSelection()->clear();
+ this->dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+
+ } else { // switch tool
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ Geom::Point const p(desktop->w2d(button_pt));
+ tools_switch_by_item (desktop, clicked_item, p);
+ }
+ } else {
+ sp_select_context_up_one_layer(desktop);
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ // save drag origin
+ xp = (gint) event->button.x;
+ yp = (gint) event->button.y;
+ within_tolerance = true;
+
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ Geom::Point const p(desktop->w2d(button_pt));
+
+ if (event->button.state & GDK_MOD1_MASK) {
+ Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ }
+
+ Inkscape::Rubberband::get(desktop)->start(desktop, p);
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK,
+ nullptr, event->button.time);
+
+ this->grabbed = SP_CANVAS_ITEM(desktop->acetate);
+
+ // remember what modifiers were on before button press
+ this->button_press_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false;
+ this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false;
+ this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false;
+
+ this->moved = FALSE;
+
+ rb_escaped = drag_escaped = 0;
+
+ ret = TRUE;
+ } else if (event->button.button == 3) {
+ // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband
+ this->sp_select_context_abort();
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ {
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ Geom::Point const motion_pt(event->motion.x, event->motion.y);
+ Geom::Point const p(desktop->w2d(motion_pt));
+ if ( within_tolerance
+ && ( abs( (gint) event->motion.x - xp ) < tolerance )
+ && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ if (this->button_press_ctrl || (this->button_press_alt && !this->button_press_shift && !selection->isEmpty())) {
+ // if it's not click and ctrl or alt was pressed (the latter with some selection
+ // but not with shift) we want to drag rather than rubberband
+ this->dragging = TRUE;
+
+ GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+
+ gdk_window_set_cursor(window, CursorSelectDragging);
+ }
+
+ if (this->dragging) {
+ /* User has dragged fast, so we get events on root (lauris)*/
+ // not only that; we will end up here when ctrl-dragging as well
+ // and also when we started within tolerance, but trespassed tolerance outside of item
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+ this->defaultMessageContext()->clear();
+
+ item_at_point = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), FALSE);
+
+ if (!item_at_point) { // if no item at this point, try at the click point (bug 1012200)
+ item_at_point = desktop->getItemAtPoint(Geom::Point(xp, yp), FALSE);
+ }
+
+ if (item_at_point || this->moved || this->button_press_alt) {
+ // drag only if starting from an item, or if something is already grabbed, or if alt-dragging
+ if (!this->moved) {
+ item_in_group = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE);
+ group_at_point = desktop->getGroupAtPoint(Geom::Point(event->button.x, event->button.y));
+
+ {
+ SPGroup *selGroup = dynamic_cast<SPGroup *>(selection->single());
+ if (selGroup && (selGroup->layerMode() == SPGroup::LAYER)) {
+ group_at_point = selGroup;
+ }
+ }
+
+ // group-at-point is meant to be topmost item if it's a group,
+ // not topmost group of all items at point
+ if (group_at_point != item_in_group &&
+ !(group_at_point && item_at_point &&
+ group_at_point->isAncestorOf(item_at_point))) {
+ group_at_point = nullptr;
+ }
+
+ // if neither a group nor an item (possibly in a group) at point are selected, set selection to the item at point
+ if ((!item_in_group || !selection->includes(item_in_group)) &&
+ (!group_at_point || !selection->includes(group_at_point))
+ && !this->button_press_alt) {
+ // select what is under cursor
+ if (!_seltrans->isEmpty()) {
+ _seltrans->resetState();
+ }
+
+ // when simply ctrl-dragging, we don't want to go into groups
+ if (item_at_point && !selection->includes(item_at_point)) {
+ selection->set(item_at_point);
+ }
+ } // otherwise, do not change selection so that dragging selected-within-group items, as well as alt-dragging, is possible
+
+ _seltrans->grab(p, -1, -1, FALSE, TRUE);
+ this->moved = TRUE;
+ }
+
+ if (!_seltrans->isEmpty()) {
+ sp_event_context_discard_delayed_snap_event(this);
+ _seltrans->moveTo(p, event->button.state);
+ }
+
+ desktop->scroll_to_point(p);
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ ret = TRUE;
+ } else {
+ this->dragging = FALSE;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ }
+
+ } else {
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->move(p);
+
+ if (Inkscape::Rubberband::get(desktop)->getMode() == RUBBERBAND_MODE_TOUCHPATH) {
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> objects to select them; release <b>Alt</b> to switch to rubberband selection"));
+ } else {
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag around</b> objects to select them; press <b>Alt</b> to switch to touch selection"));
+ }
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ }
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE:
+ xp = yp = 0;
+
+ if ((event->button.button == 1) && (this->grabbed) && !this->space_panning) {
+ if (this->dragging) {
+ GdkWindow* window;
+
+ if (this->moved) {
+ // item has been moved
+ _seltrans->ungrab();
+ this->moved = FALSE;
+#ifdef WITH_DBUS
+ dbus_send_ping(desktop, this->item);
+#endif
+ } else if (this->item && !drag_escaped) {
+ // item has not been moved -> simply a click, do selecting
+ if (!selection->isEmpty()) {
+ if (event->button.state & GDK_SHIFT_MASK) {
+ // with shift, toggle selection
+ _seltrans->resetState();
+ selection->toggle(this->item);
+ } else {
+ SPObject* single = selection->single();
+ SPGroup *singleGroup = dynamic_cast<SPGroup *>(single);
+ // without shift, increase state (i.e. toggle scale/rotation handles)
+ if (selection->includes(this->item)) {
+ _seltrans->increaseState();
+ } else if (singleGroup && (singleGroup->layerMode() == SPGroup::LAYER) && single->isAncestorOf(this->item)) {
+ _seltrans->increaseState();
+ } else {
+ _seltrans->resetState();
+ selection->set(this->item);
+ }
+ }
+ } else { // simple or shift click, no previous selection
+ _seltrans->resetState();
+ selection->set(this->item);
+ }
+ }
+
+ this->dragging = FALSE;
+ window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+
+ gdk_window_set_cursor(window, CursorSelectMouseover);
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (this->item) {
+ sp_object_unref( this->item, nullptr);
+ }
+
+ this->item = nullptr;
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+
+ if (r->is_started() && !within_tolerance) {
+ // this was a rubberband drag
+ std::vector<SPItem*> items;
+
+ if (r->getMode() == RUBBERBAND_MODE_RECT) {
+ Geom::OptRect const b = r->getRectangle();
+ items = desktop->getDocument()->getItemsInBox(desktop->dkey, (*b) * desktop->dt2doc());
+ } else if (r->getMode() == RUBBERBAND_MODE_TOUCHPATH) {
+ items = desktop->getDocument()->getItemsAtPoints(desktop->dkey, r->getPoints());
+ }
+
+ _seltrans->resetState();
+ r->stop();
+ this->defaultMessageContext()->clear();
+
+ if (event->button.state & GDK_SHIFT_MASK) {
+ // with shift, add to selection
+ selection->addList (items);
+ } else {
+ // without shift, simply select anew
+ selection->setList (items);
+ }
+
+ } else { // it was just a click, or a too small rubberband
+ r->stop();
+
+ if (this->button_press_shift && !rb_escaped && !drag_escaped) {
+ // this was a shift+click or alt+shift+click, select what was clicked upon
+ this->button_press_shift = false;
+
+ if (this->button_press_ctrl) {
+ // go into groups, honoring Alt
+ item = sp_event_context_find_item (desktop,
+ Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, TRUE);
+ this->button_press_ctrl = FALSE;
+ } else {
+ // don't go into groups, honoring Alt
+ item = sp_event_context_find_item (desktop,
+ Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE);
+ }
+
+ if (item) {
+ selection->toggle(item);
+ item = nullptr;
+ }
+
+ } else if ((this->button_press_ctrl || this->button_press_alt) && !rb_escaped && !drag_escaped) { // ctrl+click, alt+click
+ item = sp_event_context_find_item (desktop,
+ Geom::Point(event->button.x, event->button.y), this->button_press_alt, this->button_press_ctrl);
+
+ this->button_press_ctrl = FALSE;
+ this->button_press_alt = FALSE;
+
+ if (item) {
+ if (selection->includes(item)) {
+ _seltrans->increaseState();
+ } else {
+ _seltrans->resetState();
+ selection->set(item);
+ }
+
+ item = nullptr;
+ }
+ } else { // click without shift, simply deselect, unless with Alt or something was cancelled
+ if (!selection->isEmpty()) {
+ if (!(rb_escaped) && !(drag_escaped) && !(event->button.state & GDK_MOD1_MASK)) {
+ selection->clear();
+ }
+
+ rb_escaped = 0;
+ }
+ }
+ }
+
+ ret = TRUE;
+ }
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+ // Think is not necesary now
+ // desktop->updateNow();
+ }
+
+ if (event->button.button == 1) {
+ Inkscape::Rubberband::get(desktop)->stop(); // might have been started in another tool!
+ }
+
+ this->button_press_shift = false;
+ this->button_press_ctrl = false;
+ this->button_press_alt = false;
+ break;
+
+ case GDK_SCROLL: {
+
+ GdkEventScroll *scroll_event = (GdkEventScroll*) event;
+
+ if ( ! (scroll_event->state & GDK_MOD1_MASK)) // do nothing specific if alt was not pressed
+ break;
+
+ bool shift_pressed = scroll_event->state & GDK_SHIFT_MASK;
+ is_cycling = true;
+
+ /* Rebuild list of items underneath the mouse pointer */
+ Geom::Point p = desktop->d2w(desktop->point());
+ SPItem *item = desktop->getItemAtPoint(p, true, nullptr);
+ this->cycling_items.clear();
+
+ SPItem *tmp = nullptr;
+ while(item != nullptr) {
+ this->cycling_items.push_back(item);
+ item = desktop->getItemAtPoint(p, true, item);
+ if (selection->includes(item)) tmp = item;
+ }
+
+ /* Compare current item list with item list during previous scroll ... */
+ bool item_lists_differ = this->cycling_items != this->cycling_items_cmp;
+
+ if(item_lists_differ) {
+ this->sp_select_context_reset_opacities();
+ for (auto l : this->cycling_items_cmp)
+ selection->remove(l); // deselects the previous content of the cycling loop
+ this->cycling_items_cmp = (this->cycling_items);
+
+ // set opacities in new stack
+ for(auto item : this->cycling_items) {
+ if (item) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(0.3);
+ }
+ }
+ }
+ if(!cycling_cur_item) cycling_cur_item = tmp;
+
+ this->cycling_wrap = prefs->getBool("/options/selection/cycleWrap", true);
+
+ // Cycle through the items underneath the mouse pointer, one-by-one
+ this->sp_select_context_cycle_through_items(selection, scroll_event, shift_pressed);
+
+ ret = TRUE;
+
+ GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel( GTK_WIDGET(desktop->canvas) ));
+ if (w) {
+ gtk_window_present(w);
+ gtk_widget_grab_focus (GTK_WIDGET(desktop->canvas));
+ }
+ break;
+ }
+
+ case GDK_KEY_PRESS: // keybindings for select context
+ {
+ {
+ guint keyval = get_latin_keyval(&event->key);
+
+ bool alt = ( MOD__ALT(event)
+ || (keyval == GDK_KEY_Alt_L)
+ || (keyval == GDK_KEY_Alt_R)
+ || (keyval == GDK_KEY_Meta_L)
+ || (keyval == GDK_KEY_Meta_R));
+
+ if (!key_is_a_modifier (keyval)) {
+ this->defaultMessageContext()->clear();
+ } else if (this->grabbed || _seltrans->isGrabbed()) {
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ // if Alt then change cursor to moving cursor:
+ if (alt) {
+ Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ }
+ } else {
+ // do not change the statusbar text when mousekey is down to move or transform the object,
+ // because the statusbar text is already updated somewhere else.
+ break;
+ }
+ } else {
+ sp_event_show_modifier_tip (this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: click to select in groups; drag to move hor/vert"),
+ _("<b>Shift</b>: click to toggle select; drag for rubberband selection"),
+ _("<b>Alt</b>: click to select under; scroll mouse-wheel to cycle-select; drag to move selected or select by touch"));
+
+ // if Alt and nonempty selection, show moving cursor ("move selected"):
+ if (alt && !selection->isEmpty() && !desktop->isWaitingCursor()) {
+ GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+
+ gdk_window_set_cursor(window, CursorSelectDragging);
+ }
+ //*/
+ break;
+ }
+ }
+
+ gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+ gdouble const offset = prefs->getDoubleLimited("/options/defaultscale/value", 2, 0, 1000, "px");
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+ auto const y_dir = desktop->yaxisdir();
+
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Left: // move selection left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events( get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->moveScreen(mul*-10, 0); // shift
+ } else {
+ desktop->getSelection()->moveScreen(mul*-1, 0); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->move(mul*-10*nudge, 0); // shift
+ } else {
+ desktop->getSelection()->move(mul*-nudge, 0); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Up: // move selection up
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ mul *= -y_dir;
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->moveScreen(0, mul*10); // shift
+ } else {
+ desktop->getSelection()->moveScreen(0, mul*1); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->move(0, mul*10*nudge); // shift
+ } else {
+ desktop->getSelection()->move(0, mul*nudge); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Right: // move selection right
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->moveScreen(mul*10, 0); // shift
+ } else {
+ desktop->getSelection()->moveScreen(mul*1, 0); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->move(mul*10*nudge, 0); // shift
+ } else {
+ desktop->getSelection()->move(mul*nudge, 0); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Down: // move selection down
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ mul *= -y_dir;
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->moveScreen(0, mul*-10); // shift
+ } else {
+ desktop->getSelection()->moveScreen(0, mul*-1); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ desktop->getSelection()->move(0, mul*-10*nudge); // shift
+ } else {
+ desktop->getSelection()->move(0, mul*-nudge); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (!this->sp_select_context_abort()) {
+ selection->clear();
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (MOD__CTRL_ONLY(event)) {
+ sp_edit_select_all(desktop);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ /* stamping mode: show outline mode moving */
+ /* FIXME: Is next condition ok? (lauris) */
+ if (this->dragging && this->grabbed) {
+ _seltrans->stamp();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("select-x");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_bracketleft:
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ selection->rotateScreen(-mul * y_dir);
+ } else if (MOD__CTRL(event)) {
+ selection->rotate(-90 * y_dir);
+ } else if (snaps) {
+ selection->rotate(-180.0/snaps * y_dir);
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_bracketright:
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ selection->rotateScreen(mul * y_dir);
+ } else if (MOD__CTRL(event)) {
+ selection->rotate(90 * y_dir);
+ } else if (snaps) {
+ selection->rotate(180.0/snaps * y_dir);
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_Return:
+ if (MOD__CTRL_ONLY(event)) {
+ if (selection->singleItem()) {
+ SPItem *clicked_item = selection->singleItem();
+ SPGroup *clickedGroup = dynamic_cast<SPGroup *>(clicked_item);
+ if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || dynamic_cast<SPBox3D *>(clicked_item)) { // enter group or a 3D box
+ desktop->setCurrentLayer(clicked_item);
+ desktop->getSelection()->clear();
+ } else {
+ this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selected object is not a group. Cannot enter."));
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_BackSpace:
+ if (MOD__CTRL_ONLY(event)) {
+ sp_select_context_up_one_layer(desktop);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+ if (MOD__SHIFT_ONLY(event)) {
+ if (!selection->isEmpty()) {
+ _seltrans->increaseState();
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ case GDK_KEY_RELEASE: {
+ guint keyval = get_latin_keyval(&event->key);
+ if (key_is_a_modifier (keyval)) {
+ this->defaultMessageContext()->clear();
+ }
+
+ bool alt = ( MOD__ALT(event)
+ || (keyval == GDK_KEY_Alt_L)
+ || (keyval == GDK_KEY_Alt_R)
+ || (keyval == GDK_KEY_Meta_L)
+ || (keyval == GDK_KEY_Meta_R));
+
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ // if Alt then change cursor to moving cursor:
+ if (alt) {
+ Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_RECT);
+ }
+ } else {
+ if (alt) {
+ // quit cycle-selection and reset opacities
+ if (is_cycling) {
+ this->sp_select_context_reset_opacities();
+ is_cycling = false;
+ }
+ }
+ }
+
+ // set cursor to default.
+ if (!desktop->isWaitingCursor()) {
+ // Do we need to reset the cursor here on key release ?
+ //GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas()));
+ //gdk_window_set_cursor(window, event_context->cursor);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+}
+}
+}
+
+/*
+ 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/tools/select-tool.h b/src/ui/tools/select-tool.h
new file mode 100644
index 0000000..c721771
--- /dev/null
+++ b/src/ui/tools/select-tool.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_SELECT_CONTEXT_H__
+#define __SP_SELECT_CONTEXT_H__
+
+/*
+ * Select tool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+
+#define SP_SELECT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SelectTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_SELECT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SelectTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+struct SPCanvasItem;
+
+namespace Inkscape {
+ class SelTrans;
+ class SelectionDescriber;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class SelectTool : public ToolBase {
+public:
+ SelectTool();
+ ~SelectTool() override;
+
+ bool dragging;
+ bool moved;
+ bool button_press_shift;
+ bool button_press_ctrl;
+ bool button_press_alt;
+
+ std::vector<SPItem *> cycling_items;
+ std::vector<SPItem *> cycling_items_cmp;
+ SPItem *cycling_cur_item;
+ bool cycling_wrap;
+
+ SPItem *item;
+ SPCanvasItem *grabbed;
+ Inkscape::SelTrans *_seltrans;
+ Inkscape::SelectionDescriber *_describer;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ bool sp_select_context_abort();
+ void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event, bool shift_pressed);
+ void sp_select_context_reset_opacities();
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp
new file mode 100644
index 0000000..aa65c2e
--- /dev/null
+++ b/src/ui/tools/spiral-tool.cpp
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Spiral drawing context
+ *
+ * Authors:
+ * Mitsuru Oka
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2001-2002 Mitsuru Oka
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "display/sp-canvas-item.h"
+#include "display/sp-canvas.h"
+
+#include "include/macros.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-spiral.h"
+
+#include "ui/pixmaps/cursor-spiral.xpm"
+#include "ui/shape-editor.h"
+#include "ui/tools/spiral-tool.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& SpiralTool::getPrefsPath() {
+ return SpiralTool::prefsPath;
+}
+
+const std::string SpiralTool::prefsPath = "/tools/shapes/spiral";
+
+SpiralTool::SpiralTool()
+ : ToolBase(cursor_spiral_xpm)
+ , spiral(nullptr)
+ , revo(3)
+ , exp(1)
+ , t0(0)
+{
+}
+
+void SpiralTool::finish() {
+ SPDesktop *desktop = this->desktop;
+
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ ToolBase::finish();
+}
+
+SpiralTool::~SpiralTool() {
+ this->enableGrDrag(false);
+
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->spiral) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ */
+void SpiralTool::selection_changed(Inkscape::Selection *selection) {
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+}
+
+void SpiralTool::setup() {
+ ToolBase::setup();
+
+ sp_event_context_read(this, "expansion");
+ sp_event_context_read(this, "revolution");
+ sp_event_context_read(this, "t0");
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+ this->sel_changed_connection.disconnect();
+
+ this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &SpiralTool::selection_changed));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void SpiralTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring name = val.getEntryName();
+
+ if (name == "expansion") {
+ this->exp = CLAMP(val.getDouble(), 0.0, 1000.0);
+ } else if (name == "revolution") {
+ this->revo = CLAMP(val.getDouble(3.0), 0.05, 40.0);
+ } else if (name == "t0") {
+ this->t0 = CLAMP(val.getDouble(), 0.0, 0.999);
+ }
+}
+
+bool SpiralTool::root_handler(GdkEvent* event) {
+ static gboolean dragging;
+
+ SPDesktop *desktop = this->desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = TRUE;
+
+ this->center = Inkscape::setup_for_drag_start(desktop, this, event);
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ ( GDK_KEY_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_POINTER_MOTION_HINT_MASK |
+ GDK_BUTTON_PRESS_MASK ),
+ nullptr, event->button.time);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(this->desktop->w2d(motion_w));
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, this->spiral);
+ m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ this->drag(motion_dt, event->motion.state);
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ ret = TRUE;
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = FALSE;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the spiral
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+ sp_event_show_modifier_tip(this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: snap angle"),
+ nullptr,
+ _("<b>Alt</b>: lock spiral radius"));
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("spiral-revolutions");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (dragging) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the spiral
+ this->finish();
+ }
+ // do not return true, so that space would work switching to selector
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void SpiralTool::drag(Geom::Point const &p, guint state) {
+ SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ if (!this->spiral) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return;
+ }
+
+ // Create object
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ repr->setAttribute("sodipodi:type", "spiral");
+
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/spiral", false);
+
+ this->spiral = SP_SPIRAL(desktop->currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ this->spiral->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ this->spiral->updateRepr();
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ }
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, this->spiral);
+ Geom::Point pt2g = p;
+ m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ Geom::Point const p0 = desktop->dt2doc(this->center);
+ Geom::Point const p1 = desktop->dt2doc(pt2g);
+
+ Geom::Point const delta = p1 - p0;
+ gdouble const rad = Geom::L2(delta);
+
+ // Start angle calculated from end angle and number of revolutions.
+ gdouble arg = Geom::atan2(delta) - 2.0*M_PI * spiral->revo;
+
+ if (state & GDK_CONTROL_MASK) {
+ /* Snap start angle */
+ double snaps_radian = M_PI/snaps;
+ arg = std::round(arg/snaps_radian) * snaps_radian;
+ }
+
+ /* Fixme: these parameters should be got from dialog box */
+ this->spiral->setPosition(p0[Geom::X], p0[Geom::Y],
+ /*expansion*/ this->exp,
+ /*revolution*/ this->revo,
+ rad, arg,
+ /*t0*/ this->t0);
+
+ /* status text */
+ Inkscape::Util::Quantity q = Inkscape::Util::Quantity(rad, "px");
+ Glib::ustring rads = q.string(desktop->namedview->display_units);
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ _("<b>Spiral</b>: radius %s, angle %.2f&#176;; with <b>Ctrl</b> to snap angle"),
+ rads.c_str(), arg * 180/M_PI + 360*spiral->revo);
+}
+
+void SpiralTool::finishItem() {
+ this->message_context->clear();
+
+ if (this->spiral != nullptr) {
+ if (this->spiral->rad == 0) {
+ this->cancel(); // Don't allow the creating of zero sized spiral, for example when the start and and point snap to the snap grid point
+ return;
+ }
+
+ spiral->set_shape();
+ spiral->updateRepr(SP_OBJECT_WRITE_EXT);
+ spiral->doWriteTransform(spiral->transform, nullptr, true);
+
+ this->desktop->canvas->endForcedFullRedraws();
+
+ this->desktop->getSelection()->set(this->spiral);
+
+ DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_SPIRAL, _("Create spiral"));
+
+ this->spiral = nullptr;
+ }
+}
+
+void SpiralTool::cancel() {
+ this->desktop->getSelection()->clear();
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate));
+
+ if (this->spiral != nullptr) {
+ this->spiral->deleteObject();
+ this->spiral = nullptr;
+ }
+
+ this->within_tolerance = false;
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+
+ this->desktop->canvas->endForcedFullRedraws();
+
+ DocumentUndo::cancel(this->desktop->getDocument());
+}
+
+}
+}
+}
+
+/*
+ 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/tools/spiral-tool.h b/src/ui/tools/spiral-tool.h
new file mode 100644
index 0000000..03c3ae2
--- /dev/null
+++ b/src/ui/tools/spiral-tool.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_SPIRAL_CONTEXT_H__
+#define __SP_SPIRAL_CONTEXT_H__
+
+/** \file
+ * Spiral drawing context
+ */
+/*
+ * Authors:
+ * Mitsuru Oka
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2001-2002 Mitsuru Oka
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/connection.h>
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+
+#define SP_SPIRAL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SpiralTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_SPIRAL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SpiralTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+class SPSpiral;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class SpiralTool : public ToolBase {
+public:
+ SpiralTool();
+ ~SpiralTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPSpiral * spiral;
+ Geom::Point center;
+ gdouble revo;
+ gdouble exp;
+ gdouble t0;
+
+ sigc::connection sel_changed_connection;
+
+ void drag(Geom::Point const &p, guint state);
+ void finishItem();
+ void cancel();
+ void selection_changed(Inkscape::Selection *selection);
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp
new file mode 100644
index 0000000..0c95d68
--- /dev/null
+++ b/src/ui/tools/spray-tool.cpp
@@ -0,0 +1,1533 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Spray Tool
+ *
+ * Authors:
+ * Pierre-Antoine MARC
+ * Pierre CACLIN
+ * Aurel-Aimé MARMION
+ * Julien LERAY
+ * BenoĂźt LAVORATA
+ * Vincent MONTAGNE
+ * Pierre BARBRY-BLOT
+ * Steren GIANNINI (steren.giannini@gmail.com)
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ * Adrian Boguszewski
+ *
+ * Copyright (C) 2009 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <numeric>
+#include <vector>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/circle.h>
+
+
+#include "context-fns.h"
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "path-chemistry.h"
+#include "selection.h"
+#include "splivarot.h"
+#include "verbs.h"
+
+#include "display/cairo-utils.h"
+#include "display/canvas-arena.h"
+#include "display/curve.h"
+#include "display/drawing-context.h"
+#include "display/drawing.h"
+#include "display/sp-canvas.h"
+
+#include "helper/action.h"
+
+#include "object/box3d.h"
+#include "object/sp-item-transform.h"
+
+#include "ui/pixmaps/cursor-spray.xpm"
+
+#include "svg/svg.h"
+#include "svg/svg-color.h"
+
+#include "ui/toolbar/spray-toolbar.h"
+#include "ui/tools/spray-tool.h"
+#include "ui/dialog/dialog-manager.h"
+
+
+using Inkscape::DocumentUndo;
+
+#define DDC_RED_RGBA 0xff0000ff
+#define DYNA_MIN_WIDTH 1.0e-6
+
+// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object
+// with the mode: spray object in single path)
+// Please enable again when working on 1.0
+#define ENABLE_SPRAY_MODE_SINGLE_PATH
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum {
+ PICK_COLOR,
+ PICK_OPACITY,
+ PICK_R,
+ PICK_G,
+ PICK_B,
+ PICK_H,
+ PICK_S,
+ PICK_L
+};
+
+const std::string& SprayTool::getPrefsPath() {
+ return SprayTool::prefsPath;
+}
+
+const std::string SprayTool::prefsPath = "/tools/spray";
+
+/**
+ * This function returns pseudo-random numbers from a normal distribution
+ * @param mu : mean
+ * @param sigma : standard deviation ( > 0 )
+ */
+inline double NormalDistribution(double mu, double sigma)
+{
+ // use Box Muller's algorithm
+ return mu + sigma * sqrt( -2.0 * log(g_random_double_range(0, 1)) ) * cos( 2.0*M_PI*g_random_double_range(0, 1) );
+}
+
+/* Method to rotate items */
+static void sp_spray_rotate_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Rotate const &rotation)
+{
+ Geom::Translate const s(c);
+ Geom::Affine affine = s.inverse() * rotation * s;
+ // Rotate item.
+ item->set_i2d_affine(item->i2dt_affine() * affine);
+ // Use each item's own transform writer, consistent with sp_selection_apply_affine()
+ item->doWriteTransform(item->transform);
+ // Restore the center position (it's changed because the bbox center changed)
+ if (item->isCenterSet()) {
+ item->setCenter(c);
+ item->updateRepr();
+ }
+}
+
+/* Method to scale items */
+static void sp_spray_scale_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Scale const &scale)
+{
+ Geom::Translate const s(c);
+ item->set_i2d_affine(item->i2dt_affine() * s.inverse() * scale * s);
+ item->doWriteTransform(item->transform);
+}
+
+SprayTool::SprayTool()
+ : ToolBase(cursor_spray_xpm, false)
+ , pressure(TC_DEFAULT_PRESSURE)
+ , dragging(false)
+ , usepressurewidth(false)
+ , usepressurepopulation(false)
+ , usepressurescale(false)
+ , usetilt(false)
+ , usetext(false)
+ , width(0.2)
+ , ratio(0)
+ , tilt(0)
+ , rotation_variation(0)
+ , population(0)
+ , scale_variation(1)
+ , scale(1)
+ , mean(0.2)
+ , standard_deviation(0.2)
+ , distrib(1)
+ , mode(0)
+ , is_drawing(false)
+ , is_dilating(false)
+ , has_dilated(false)
+ , dilate_area(nullptr)
+ , no_overlap(false)
+ , picker(false)
+ , pick_center(true)
+ , pick_inverse_value(false)
+ , pick_fill(false)
+ , pick_stroke(false)
+ , pick_no_overlap(false)
+ , over_transparent(true)
+ , over_no_transparent(true)
+ , offset(0)
+ , pick(0)
+ , do_trace(false)
+ , pick_to_size(false)
+ , pick_to_presence(false)
+ , pick_to_color(false)
+ , pick_to_opacity(false)
+ , invert_picked(false)
+ , gamma_picked(0)
+ , rand_picked(0)
+{
+}
+
+SprayTool::~SprayTool() {
+ object_set.clear();
+ this->enableGrDrag(false);
+ this->style_set_connection.disconnect();
+
+ if (this->dilate_area) {
+ sp_canvas_item_destroy(this->dilate_area);
+ this->dilate_area = nullptr;
+ }
+}
+
+void SprayTool::update_cursor(bool /*with_shift*/) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!desktop->selection->isEmpty()) {
+ num = (guint) boost::distance(desktop->selection->items());
+ sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num);
+ } else {
+ sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected"));
+ }
+
+ switch (this->mode) {
+ case SPRAY_MODE_COPY:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>copies</b> of the initial selection."), sel_message);
+ break;
+ case SPRAY_MODE_CLONE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>clones</b> of the initial selection."), sel_message);
+ break;
+ case SPRAY_MODE_SINGLE_PATH:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray in a <b>single path</b> of the initial selection."), sel_message);
+ break;
+ default:
+ break;
+ }
+
+ this->sp_event_context_update_cursor();
+ g_free(sel_message);
+}
+
+void SprayTool::setup() {
+ ToolBase::setup();
+
+ {
+ /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1));
+
+ SPCurve *c = new SPCurve(path);
+
+ this->dilate_area = sp_canvas_bpath_new(this->desktop->getControls(), c);
+ c->unref();
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_hide(this->dilate_area);
+ }
+
+ this->is_drawing = false;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/clonetiler/dotrace", false);
+ if (prefs->getBool("/tools/spray/selcue")) {
+ this->enableSelectionCue();
+ }
+ if (prefs->getBool("/tools/spray/gradientdrag")) {
+ this->enableGrDrag();
+ }
+
+ sp_event_context_read(this, "distrib");
+ sp_event_context_read(this, "width");
+ sp_event_context_read(this, "ratio");
+ sp_event_context_read(this, "tilt");
+ sp_event_context_read(this, "rotation_variation");
+ sp_event_context_read(this, "scale_variation");
+ sp_event_context_read(this, "mode");
+ sp_event_context_read(this, "population");
+ sp_event_context_read(this, "mean");
+ sp_event_context_read(this, "standard_deviation");
+ sp_event_context_read(this, "usepressurewidth");
+ sp_event_context_read(this, "usepressurepopulation");
+ sp_event_context_read(this, "usepressurescale");
+ sp_event_context_read(this, "Scale");
+ sp_event_context_read(this, "offset");
+ sp_event_context_read(this, "picker");
+ sp_event_context_read(this, "pick_center");
+ sp_event_context_read(this, "pick_inverse_value");
+ sp_event_context_read(this, "pick_fill");
+ sp_event_context_read(this, "pick_stroke");
+ sp_event_context_read(this, "pick_no_overlap");
+ sp_event_context_read(this, "over_no_transparent");
+ sp_event_context_read(this, "over_transparent");
+ sp_event_context_read(this, "no_overlap");
+}
+
+void SprayTool::setCloneTilerPrefs() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->do_trace = prefs->getBool("/dialogs/clonetiler/dotrace", false);
+ this->pick = prefs->getInt("/dialogs/clonetiler/pick");
+ this->pick_to_size = prefs->getBool("/dialogs/clonetiler/pick_to_size", false);
+ this->pick_to_presence = prefs->getBool("/dialogs/clonetiler/pick_to_presence", false);
+ this->pick_to_color = prefs->getBool("/dialogs/clonetiler/pick_to_color", false);
+ this->pick_to_opacity = prefs->getBool("/dialogs/clonetiler/pick_to_opacity", false);
+ this->rand_picked = 0.01 * prefs->getDoubleLimited("/dialogs/clonetiler/rand_picked", 0, 0, 100);
+ this->invert_picked = prefs->getBool("/dialogs/clonetiler/invert_picked", false);
+ this->gamma_picked = prefs->getDoubleLimited("/dialogs/clonetiler/gamma_picked", 0, -10, 10);
+}
+
+void SprayTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "mode") {
+ this->mode = val.getInt();
+ this->update_cursor(false);
+ } else if (path == "width") {
+ this->width = 0.01 * CLAMP(val.getInt(10), 1, 100);
+ } else if (path == "usepressurewidth") {
+ this->usepressurewidth = val.getBool();
+ } else if (path == "usepressurepopulation") {
+ this->usepressurepopulation = val.getBool();
+ } else if (path == "usepressurescale") {
+ this->usepressurescale = val.getBool();
+ } else if (path == "population") {
+ this->population = 0.01 * CLAMP(val.getInt(10), 1, 100);
+ } else if (path == "rotation_variation") {
+ this->rotation_variation = CLAMP(val.getDouble(0.0), 0, 100.0);
+ } else if (path == "scale_variation") {
+ this->scale_variation = CLAMP(val.getDouble(1.0), 0, 100.0);
+ } else if (path == "standard_deviation") {
+ this->standard_deviation = 0.01 * CLAMP(val.getInt(10), 1, 100);
+ } else if (path == "mean") {
+ this->mean = 0.01 * CLAMP(val.getInt(10), 1, 100);
+// Not implemented in the toolbar and preferences yet
+ } else if (path == "distribution") {
+ this->distrib = val.getInt(1);
+ } else if (path == "tilt") {
+ this->tilt = CLAMP(val.getDouble(0.1), 0, 1000.0);
+ } else if (path == "ratio") {
+ this->ratio = CLAMP(val.getDouble(), 0.0, 0.9);
+ } else if (path == "offset") {
+ this->offset = val.getDoubleLimited(100.0, 0, 1000.0);
+ } else if (path == "pick_center") {
+ this->pick_center = val.getBool(true);
+ } else if (path == "pick_inverse_value") {
+ this->pick_inverse_value = val.getBool(false);
+ } else if (path == "pick_fill") {
+ this->pick_fill = val.getBool(false);
+ } else if (path == "pick_stroke") {
+ this->pick_stroke = val.getBool(false);
+ } else if (path == "pick_no_overlap") {
+ this->pick_no_overlap = val.getBool(false);
+ } else if (path == "over_no_transparent") {
+ this->over_no_transparent = val.getBool(true);
+ } else if (path == "over_transparent") {
+ this->over_transparent = val.getBool(true);
+ } else if (path == "no_overlap") {
+ this->no_overlap = val.getBool(false);
+ } else if (path == "picker") {
+ this->picker = val.getBool(false);
+ }
+}
+
+static void sp_spray_extinput(SprayTool *tc, GdkEvent *event)
+{
+ if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &tc->pressure)) {
+ tc->pressure = CLAMP(tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE);
+ } else {
+ tc->pressure = TC_DEFAULT_PRESSURE;
+ }
+}
+
+static double get_width(SprayTool *tc)
+{
+ double pressure = (tc->usepressurewidth? tc->pressure / TC_DEFAULT_PRESSURE : 1);
+ return pressure * tc->width;
+}
+
+static double get_dilate_radius(SprayTool *tc)
+{
+ return 250 * get_width(tc)/SP_EVENT_CONTEXT(tc)->desktop->current_zoom();
+}
+
+static double get_path_mean(SprayTool *tc)
+{
+ return tc->mean;
+}
+
+static double get_path_standard_deviation(SprayTool *tc)
+{
+ return tc->standard_deviation;
+}
+
+static double get_population(SprayTool *tc)
+{
+ double pressure = (tc->usepressurepopulation? tc->pressure / TC_DEFAULT_PRESSURE : 1);
+ return pressure * tc->population;
+}
+
+static double get_pressure(SprayTool *tc)
+{
+ double pressure = tc->pressure / TC_DEFAULT_PRESSURE;
+ return pressure;
+}
+
+static double get_move_mean(SprayTool *tc)
+{
+ return tc->mean;
+}
+
+static double get_move_standard_deviation(SprayTool *tc)
+{
+ return tc->standard_deviation;
+}
+
+/**
+ * Method to handle the distribution of the items
+ * @param[out] radius : radius of the position of the sprayed object
+ * @param[out] angle : angle of the position of the sprayed object
+ * @param[in] a : mean
+ * @param[in] s : standard deviation
+ * @param[in] choice :
+
+ */
+static void random_position(double &radius, double &angle, double &a, double &s, int /*choice*/)
+{
+ // angle is taken from an uniform distribution
+ angle = g_random_double_range(0, M_PI*2.0);
+
+ // radius is taken from a Normal Distribution
+ double radius_temp =-1;
+ while(!((radius_temp >= 0) && (radius_temp <=1 )))
+ {
+ radius_temp = NormalDistribution(a, s);
+ }
+ // Because we are in polar coordinates, a special treatment has to be done to the radius.
+ // Otherwise, positions taken from an uniform repartition on radius and angle will not seam to
+ // be uniformily distributed on the disk (more at the center and less at the boundary).
+ // We counter this effect with a 0.5 exponent. This is empiric.
+ radius = pow(radius_temp, 0.5);
+
+}
+
+static void sp_spray_transform_path(SPItem * item, Geom::Path &path, Geom::Affine affine, Geom::Point center){
+ path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr).inverse();
+ path *= item->transform.inverse();
+ Geom::Affine dt2p;
+ if (item->parent) {
+ dt2p = static_cast<SPItem *>(item->parent)->i2dt_affine().inverse();
+ } else {
+ dt2p = item->document->dt2doc();
+ }
+ Geom::Affine i2dt = item->i2dt_affine() * Geom::Translate(center).inverse() * affine * Geom::Translate(center);
+ path *= i2dt * dt2p;
+ path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr);
+}
+
+/**
+Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same
+probability of being displaced.
+ */
+double 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...
+}
+
+guint32 getPickerData(Geom::IntRect area){
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ double R = 0, G = 0, B = 0, A = 0;
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area.width(), area.height());
+ sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area);
+ ink_cairo_surface_average_color(s, R, G, B, A);
+ cairo_surface_destroy(s);
+ //this can fix the bug #1511998 if confirmed
+ if( A == 0 || A < 1e-6){
+ R = 1;
+ G = 1;
+ B = 1;
+ }
+ return SP_RGBA32_F_COMPOSE(R, G, B, A);
+}
+
+static void showHidden(std::vector<SPItem *> items_down){
+ for (auto item_hidden : items_down) {
+ item_hidden->setHidden(false);
+ item_hidden->updateRepr();
+ }
+}
+//todo: maybe move same parameter to preferences
+static bool fit_item(SPDesktop *desktop,
+ SPItem *item,
+ Geom::OptRect bbox,
+ Geom::Point &move,
+ Geom::Point center,
+ gint mode,
+ double angle,
+ double &_scale,
+ double scale,
+ bool picker,
+ bool pick_center,
+ bool pick_inverse_value,
+ bool pick_fill,
+ bool pick_stroke,
+ bool pick_no_overlap,
+ bool over_no_transparent,
+ bool over_transparent,
+ bool no_overlap,
+ double offset,
+ SPCSSAttr *css,
+ bool trace_scale,
+ int pick,
+ bool do_trace,
+ bool pick_to_size,
+ bool pick_to_presence,
+ bool pick_to_color,
+ bool pick_to_opacity,
+ bool invert_picked,
+ double gamma_picked ,
+ double rand_picked)
+{
+ SPDocument *doc = item->document;
+ double width = bbox->width();
+ double height = bbox->height();
+ double offset_width = (offset * width)/100.0 - (width);
+ if(offset_width < 0 ){
+ offset_width = 0;
+ }
+ double offset_height = (offset * height)/100.0 - (height);
+ if(offset_height < 0 ){
+ offset_height = 0;
+ }
+ if(picker && pick_to_size && !trace_scale && do_trace){
+ _scale = 0.1;
+ }
+ Geom::OptRect bbox_procesed = Geom::Rect(Geom::Point(bbox->left() - offset_width, bbox->top() - offset_height),Geom::Point(bbox->right() + offset_width, bbox->bottom() + offset_height));
+ Geom::Path path;
+ path.start(Geom::Point(bbox_procesed->left(), bbox_procesed->top()));
+ path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->top()));
+ path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->bottom()));
+ path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->left(), bbox_procesed->bottom()));
+ path.close(true);
+ sp_spray_transform_path(item, path, Geom::Scale(_scale), center);
+ sp_spray_transform_path(item, path, Geom::Scale(scale), center);
+ sp_spray_transform_path(item, path, Geom::Rotate(angle), center);
+ path *= Geom::Translate(move);
+ bbox_procesed = path.boundsFast();
+ double bbox_left_main = bbox_procesed->left();
+ double bbox_right_main = bbox_procesed->right();
+ double bbox_top_main = bbox_procesed->top();
+ double bbox_bottom_main = bbox_procesed->bottom();
+ double width_transformed = bbox_procesed->width();
+ double height_transformed = bbox_procesed->height();
+ Geom::Point mid_point = desktop->d2w(bbox_procesed->midpoint() * desktop->doc2dt());
+ Geom::IntRect area = Geom::IntRect::from_xywh(floor(mid_point[Geom::X]), floor(mid_point[Geom::Y]), 1, 1);
+ guint32 rgba = getPickerData(area);
+ guint32 rgba2 = 0xffffff00;
+ Geom::Rect rect_sprayed(desktop->d2w(Geom::Point(bbox_left_main,bbox_top_main)), desktop->d2w(Geom::Point(bbox_right_main,bbox_bottom_main)));
+ if (!rect_sprayed.hasZeroArea()) {
+ rgba2 = getPickerData(rect_sprayed.roundOutwards());
+ }
+ if(pick_no_overlap) {
+ if(rgba != rgba2) {
+ if(mode != SPRAY_MODE_ERASER) {
+ return false;
+ }
+ }
+ }
+ if(!pick_center) {
+ rgba = rgba2;
+ }
+ if(!over_transparent && (SP_RGBA32_A_F(rgba) == 0 || SP_RGBA32_A_F(rgba) < 1e-6)) {
+ if(mode != SPRAY_MODE_ERASER) {
+ return false;
+ }
+ }
+ if(!over_no_transparent && SP_RGBA32_A_F(rgba) > 0) {
+ if(mode != SPRAY_MODE_ERASER) {
+ return false;
+ }
+ }
+ if(offset < 100 ) {
+ offset_width = ((99.0 - offset) * width_transformed)/100.0 - width_transformed;
+ offset_height = ((99.0 - offset) * height_transformed)/100.0 - height_transformed;
+ } else {
+ offset_width = 0;
+ offset_height = 0;
+ }
+ std::vector<SPItem*> items_down = desktop->getDocument()->getItemsPartiallyInBox(desktop->dkey, *bbox_procesed);
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (selection->isEmpty()) {
+ return false;
+ }
+ std::vector<SPItem*> const items_selected(selection->items().begin(), selection->items().end());
+ std::vector<SPItem*> items_down_erased;
+ for (std::vector<SPItem*>::const_iterator i=items_down.begin(); i!=items_down.end(); ++i) {
+ SPItem *item_down = *i;
+ Geom::OptRect bbox_down = item_down->documentVisualBounds();
+ double bbox_left = bbox_down->left();
+ double bbox_top = bbox_down->top();
+ gchar const * item_down_sharp = g_strdup_printf("#%s", item_down->getId());
+ items_down_erased.push_back(item_down);
+ for (auto item_selected : items_selected) {
+ gchar const * spray_origin;
+ if(!item_selected->getAttribute("inkscape:spray-origin")){
+ spray_origin = g_strdup_printf("#%s", item_selected->getId());
+ } else {
+ spray_origin = item_selected->getAttribute("inkscape:spray-origin");
+ }
+ if(strcmp(item_down_sharp, spray_origin) == 0 ||
+ (item_down->getAttribute("inkscape:spray-origin") &&
+ strcmp(item_down->getAttribute("inkscape:spray-origin"),spray_origin) == 0 ))
+ {
+ if(mode == SPRAY_MODE_ERASER) {
+ if(strcmp(item_down_sharp, spray_origin) != 0 && !selection->includes(item_down) ){
+ item_down->deleteObject();
+ items_down_erased.pop_back();
+ break;
+ }
+ } else if(no_overlap) {
+ if(!(offset_width < 0 && offset_height < 0 && std::abs(bbox_left - bbox_left_main) > std::abs(offset_width) &&
+ std::abs(bbox_top - bbox_top_main) > std::abs(offset_height))){
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ } else if(picker || over_transparent || over_no_transparent) {
+ item_down->setHidden(true);
+ item_down->updateRepr();
+ }
+ }
+ }
+ }
+ if(mode == SPRAY_MODE_ERASER){
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down_erased);
+ }
+ return false;
+ }
+ if(picker || over_transparent || over_no_transparent){
+ if(!no_overlap){
+ doc->ensureUpToDate();
+ rgba = getPickerData(area);
+ if (!rect_sprayed.hasZeroArea()) {
+ rgba2 = getPickerData(rect_sprayed.roundOutwards());
+ }
+ }
+ if(pick_no_overlap){
+ if(rgba != rgba2){
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ }
+ if(!pick_center){
+ rgba = rgba2;
+ }
+ double opacity = 1.0;
+ gchar color_string[32]; *color_string = 0;
+ 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);
+ if(!over_transparent && (a == 0 || a < 1e-6)){
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ if(!over_no_transparent && a > 0){
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+
+ if(picker && do_trace){
+ 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 ((double)r, (double)power);
+ g = pow ((double)g, (double)power);
+ b = pow ((double)b, (double)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_size) {
+ if(!trace_scale){
+ if(pick_inverse_value) {
+ _scale = 1.0 - val;
+ } else {
+ _scale = val;
+ }
+ if(_scale == 0.0) {
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ if(!fit_item(desktop
+ , item
+ , bbox
+ , move
+ , center
+ , mode
+ , angle
+ , _scale
+ , scale
+ , picker
+ , pick_center
+ , pick_inverse_value
+ , pick_fill
+ , pick_stroke
+ , pick_no_overlap
+ , over_no_transparent
+ , over_transparent
+ , no_overlap
+ , offset
+ , css
+ , true
+ , pick
+ , do_trace
+ , pick_to_size
+ , pick_to_presence
+ , pick_to_color
+ , pick_to_opacity
+ , invert_picked
+ , gamma_picked
+ , rand_picked)
+ )
+ {
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ }
+ }
+
+ if (pick_to_opacity) {
+ if(pick_inverse_value) {
+ opacity *= 1.0 - val;
+ } else {
+ opacity *= val;
+ }
+ std::stringstream opacity_str;
+ opacity_str.imbue(std::locale::classic());
+ opacity_str << opacity;
+ sp_repr_css_set_property(css, "opacity", opacity_str.str().c_str());
+ }
+ if (pick_to_presence) {
+ if (g_random_double_range (0, 1) > val) {
+ //Hiding the element is a way to retain original
+ //behaviour of tiled clones for presence option.
+ sp_repr_css_set_property(css, "opacity", "0");
+ }
+ }
+ if (pick_to_color) {
+ sp_svg_write_color(color_string, sizeof(color_string), rgba);
+ if(pick_fill){
+ sp_repr_css_set_property(css, "fill", color_string);
+ }
+ if(pick_stroke){
+ sp_repr_css_set_property(css, "stroke", color_string);
+ }
+ }
+ if (opacity < 1e-6) { // invisibly transparent, skip
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ return false;
+ }
+ }
+ if(!do_trace){
+ if(!pick_center){
+ rgba = rgba2;
+ }
+ if (pick_inverse_value) {
+ r = 1 - SP_RGBA32_R_F(rgba);
+ g = 1 - SP_RGBA32_G_F(rgba);
+ b = 1 - SP_RGBA32_B_F(rgba);
+ } else {
+ r = SP_RGBA32_R_F(rgba);
+ g = SP_RGBA32_G_F(rgba);
+ b = SP_RGBA32_B_F(rgba);
+ }
+ rgba = SP_RGBA32_F_COMPOSE(r, g, b, a);
+ sp_svg_write_color(color_string, sizeof(color_string), rgba);
+ if(pick_fill){
+ sp_repr_css_set_property(css, "fill", color_string);
+ }
+ if(pick_stroke){
+ sp_repr_css_set_property(css, "stroke", color_string);
+ }
+ }
+ if(!no_overlap && (picker || over_transparent || over_no_transparent)){
+ showHidden(items_down);
+ }
+ }
+ return true;
+}
+
+static bool sp_spray_recursive(SPDesktop *desktop,
+ Inkscape::ObjectSet *set,
+ SPItem *item,
+ Geom::Point p,
+ Geom::Point /*vector*/,
+ gint mode,
+ double radius,
+ double population,
+ double &scale,
+ double scale_variation,
+ bool /*reverse*/,
+ double mean,
+ double standard_deviation,
+ double ratio,
+ double tilt,
+ double rotation_variation,
+ gint _distrib,
+ bool no_overlap,
+ bool picker,
+ bool pick_center,
+ bool pick_inverse_value,
+ bool pick_fill,
+ bool pick_stroke,
+ bool pick_no_overlap,
+ bool over_no_transparent,
+ bool over_transparent,
+ double offset,
+ bool usepressurescale,
+ double pressure,
+ int pick,
+ bool do_trace,
+ bool pick_to_size,
+ bool pick_to_presence,
+ bool pick_to_color,
+ bool pick_to_opacity,
+ bool invert_picked,
+ double gamma_picked ,
+ double rand_picked)
+{
+ bool did = false;
+
+ {
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ // convert 3D boxes to ordinary groups before spraying their shapes
+ item = box3d_convert_to_group(box);
+ set->add(item);
+ }
+ }
+
+ double _fid = g_random_double_range(0, 1);
+ double angle = g_random_double_range( - rotation_variation / 100.0 * M_PI , rotation_variation / 100.0 * M_PI );
+ double _scale = g_random_double_range( 1.0 - scale_variation / 100.0, 1.0 + scale_variation / 100.0 );
+ if(usepressurescale){
+ _scale = pressure;
+ }
+ double dr; double dp;
+ random_position( dr, dp, mean, standard_deviation, _distrib );
+ dr=dr*radius;
+
+ if (mode == SPRAY_MODE_COPY || mode == SPRAY_MODE_ERASER) {
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ if(_fid <= population)
+ {
+ SPDocument *doc = item->document;
+ gchar const * spray_origin;
+ if(!item->getAttribute("inkscape:spray-origin")){
+ spray_origin = g_strdup_printf("#%s", item->getId());
+ } else {
+ spray_origin = item->getAttribute("inkscape:spray-origin");
+ }
+ Geom::Point center = item->getCenter();
+ Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint());
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if(mode == SPRAY_MODE_ERASER || no_overlap || picker || !over_transparent || !over_no_transparent){
+ if(!fit_item(desktop
+ , item
+ , a
+ , move
+ , center
+ , mode
+ , angle
+ , _scale
+ , scale
+ , picker
+ , pick_center
+ , pick_inverse_value
+ , pick_fill
+ , pick_stroke
+ , pick_no_overlap
+ , over_no_transparent
+ , over_transparent
+ , no_overlap
+ , offset
+ , css
+ , false
+ , pick
+ , do_trace
+ , pick_to_size
+ , pick_to_presence
+ , pick_to_color
+ , pick_to_opacity
+ , invert_picked
+ , gamma_picked
+ , rand_picked)){
+ return false;
+ }
+ }
+ SPItem *item_copied;
+ // Duplicate
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = item->getRepr();
+ Inkscape::XML::Node *parent = old_repr->parent();
+ Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc);
+ if(!copy->attribute("inkscape:spray-origin")){
+ copy->setAttribute("inkscape:spray-origin", spray_origin);
+ }
+ parent->appendChild(copy);
+ SPObject *new_obj = doc->getObjectByRepr(copy);
+ item_copied = dynamic_cast<SPItem *>(new_obj); // Conversion object->item
+ sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(_scale));
+ sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(scale));
+ sp_spray_rotate_rel(center,desktop,item_copied, Geom::Rotate(angle));
+ // Move the cursor p
+ item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation()));
+ Inkscape::GC::release(copy);
+ if(picker){
+ sp_desktop_apply_css_recursive(item_copied, css, true);
+ }
+ did = true;
+ }
+ }
+#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH
+ } else if (mode == SPRAY_MODE_SINGLE_PATH) {
+ long setSize = boost::distance(set->items());
+ SPItem *parent_item = setSize > 0 ? set->items().front() : nullptr; // Initial object
+ SPItem *unionResult = setSize > 1 ? *(++set->items().begin()) : nullptr; // Previous union
+ SPItem *item_copied = nullptr; // Projected object
+
+ if (parent_item) {
+ SPDocument *doc = parent_item->document;
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = parent_item->getRepr();
+ Inkscape::XML::Node *parent = old_repr->parent();
+
+ Geom::OptRect a = parent_item->documentVisualBounds();
+ if (a) {
+ if (_fid <= population) { // Rules the population of objects sprayed
+ // Duplicates the parent item
+ Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc);
+ gchar const * spray_origin;
+ if(!copy->attribute("inkscape:spray-origin")){
+ spray_origin = g_strdup_printf("#%s", old_repr->attribute("id"));
+ copy->setAttribute("inkscape:spray-origin", spray_origin);
+ } else {
+ spray_origin = copy->attribute("inkscape:spray-origin");
+ }
+ parent->appendChild(copy);
+ SPObject *new_obj = doc->getObjectByRepr(copy);
+ item_copied = dynamic_cast<SPItem *>(new_obj);
+
+ // Move around the cursor
+ Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint());
+
+ Geom::Point center = parent_item->getCenter();
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale));
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale));
+ sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle));
+ item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation()));
+
+ // Union and duplication
+ set->clear();
+ set->add(item_copied);
+ if (unionResult) { // No need to add the very first item (initialized with NULL).
+ set->add(unionResult);
+ }
+ set->pathUnion(true);
+ set->add(parent_item);
+ Inkscape::GC::release(copy);
+ did = true;
+ }
+ }
+ }
+#endif
+ } else if (mode == SPRAY_MODE_CLONE) {
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ if(_fid <= population) {
+ SPDocument *doc = item->document;
+ gchar const * spray_origin;
+ if(!item->getAttribute("inkscape:spray-origin")){
+ spray_origin = g_strdup_printf("#%s", item->getId());
+ } else {
+ spray_origin = item->getAttribute("inkscape:spray-origin");
+ }
+ Geom::Point center=item->getCenter();
+ Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint());
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if(mode == SPRAY_MODE_ERASER || no_overlap || picker || !over_transparent || !over_no_transparent){
+ if(!fit_item(desktop
+ , item
+ , a
+ , move
+ , center
+ , mode
+ , angle
+ , _scale
+ , scale
+ , picker
+ , pick_center
+ , pick_inverse_value
+ , pick_fill
+ , pick_stroke
+ , pick_no_overlap
+ , over_no_transparent
+ , over_transparent
+ , no_overlap
+ , offset
+ , css
+ , true
+ , pick
+ , do_trace
+ , pick_to_size
+ , pick_to_presence
+ , pick_to_color
+ , pick_to_opacity
+ , invert_picked
+ , gamma_picked
+ , rand_picked))
+ {
+ return false;
+ }
+ }
+ SPItem *item_copied;
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = item->getRepr();
+ Inkscape::XML::Node *parent = old_repr->parent();
+
+ // Creation of the clone
+ Inkscape::XML::Node *clone = xml_doc->createElement("svg:use");
+ // Ad the clone to the list of the parent's children
+ parent->appendChild(clone);
+ // Generates the link between parent and child attributes
+ if(!clone->attribute("inkscape:spray-origin")){
+ clone->setAttribute("inkscape:spray-origin", spray_origin);
+ }
+ gchar *href_str = g_strdup_printf("#%s", old_repr->attribute("id"));
+ clone->setAttribute("xlink:href", href_str);
+ g_free(href_str);
+
+ SPObject *clone_object = doc->getObjectByRepr(clone);
+ // Conversion object->item
+ item_copied = dynamic_cast<SPItem *>(clone_object);
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale));
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale));
+ sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle));
+ item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation()));
+ if(picker){
+ sp_desktop_apply_css_recursive(item_copied, css, true);
+ }
+ Inkscape::GC::release(clone);
+ did = true;
+ }
+ }
+ }
+
+ return did;
+}
+
+static bool sp_spray_dilate(SprayTool *tc, Geom::Point /*event_p*/, Geom::Point p, Geom::Point vector, bool reverse)
+{
+ SPDesktop *desktop = tc->desktop;
+ Inkscape::ObjectSet *set = tc->objectSet();
+ if (set->isEmpty()) {
+ return false;
+ }
+
+ bool did = false;
+ double radius = get_dilate_radius(tc);
+ double population = get_population(tc);
+ if (radius == 0 || population == 0) {
+ return false;
+ }
+ double path_mean = get_path_mean(tc);
+ if (radius == 0 || path_mean == 0) {
+ return false;
+ }
+ double path_standard_deviation = get_path_standard_deviation(tc);
+ if (radius == 0 || path_standard_deviation == 0) {
+ return false;
+ }
+ double move_mean = get_move_mean(tc);
+ double move_standard_deviation = get_move_standard_deviation(tc);
+
+ {
+ std::vector<SPItem*> const items(set->items().begin(), set->items().end());
+
+ for(auto item : items){
+ g_assert(item != nullptr);
+ sp_object_ref(item);
+ }
+
+ for(auto item : items){
+ g_assert(item != nullptr);
+ if (sp_spray_recursive(desktop
+ , set
+ , item
+ , p, vector
+ , tc->mode
+ , radius
+ , population
+ , tc->scale
+ , tc->scale_variation
+ , reverse
+ , move_mean
+ , move_standard_deviation
+ , tc->ratio
+ , tc->tilt
+ , tc->rotation_variation
+ , tc->distrib
+ , tc->no_overlap
+ , tc->picker
+ , tc->pick_center
+ , tc->pick_inverse_value
+ , tc->pick_fill
+ , tc->pick_stroke
+ , tc->pick_no_overlap
+ , tc->over_no_transparent
+ , tc->over_transparent
+ , tc->offset
+ , tc->usepressurescale
+ , get_pressure(tc)
+ , tc->pick
+ , tc->do_trace
+ , tc->pick_to_size
+ , tc->pick_to_presence
+ , tc->pick_to_color
+ , tc->pick_to_opacity
+ , tc->invert_picked
+ , tc->gamma_picked
+ , tc->rand_picked)) {
+ did = true;
+ }
+ }
+
+ for(auto item : items){
+ g_assert(item != nullptr);
+ sp_object_unref(item);
+ }
+ }
+
+ return did;
+}
+
+static void sp_spray_update_area(SprayTool *tc)
+{
+ double radius = get_dilate_radius(tc);
+ Geom::Affine const sm ( Geom::Scale(radius/(1-tc->ratio), radius/(1+tc->ratio)) );
+ sp_canvas_item_affine_absolute(tc->dilate_area, (sm* Geom::Rotate(tc->tilt))* Geom::Translate(SP_EVENT_CONTEXT(tc)->desktop->point()));
+ sp_canvas_item_show(tc->dilate_area);
+}
+
+static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift)
+{
+ // Select the button mode
+ auto tb = dynamic_cast<UI::Toolbar::SprayToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("SprayToolbar"));
+
+ if(tb) {
+ tb->set_mode(mode);
+ } else {
+ std::cerr << "Could not access Spray toolbar" << std::endl;
+ }
+
+ // Need to set explicitly, because the prefs may not have changed by the previous
+ tc->mode = mode;
+ tc->update_cursor(with_shift);
+}
+
+bool SprayTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ sp_canvas_item_show(this->dilate_area);
+ break;
+ case GDK_LEAVE_NOTIFY:
+ sp_canvas_item_hide(this->dilate_area);
+ break;
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+ this->setCloneTilerPrefs();
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+ this->last_push = desktop->dt2doc(motion_dt);
+
+ sp_spray_extinput(this, event);
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(3);
+ set_high_motion_precision();
+ this->is_drawing = true;
+ this->is_dilating = true;
+ this->has_dilated = false;
+
+ object_set = *desktop->getSelection();
+ if (mode == SPRAY_MODE_SINGLE_PATH) {
+ desktop->getSelection()->clear();
+ }
+
+ sp_spray_dilate(this, motion_w, this->last_push, Geom::Point(0,0), MOD__SHIFT(event));
+
+ this->has_dilated = true;
+ ret = TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY: {
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ Geom::Point motion_doc(desktop->dt2doc(motion_dt));
+ sp_spray_extinput(this, event);
+
+ // Draw the dilating cursor
+ double radius = get_dilate_radius(this);
+ Geom::Affine const sm (Geom::Scale(radius/(1-this->ratio), radius/(1+this->ratio)) );
+ sp_canvas_item_affine_absolute(this->dilate_area, (sm*Geom::Rotate(this->tilt))*Geom::Translate(desktop->w2d(motion_w)));
+ sp_canvas_item_show(this->dilate_area);
+
+ guint num = 0;
+ if (!desktop->selection->isEmpty()) {
+ num = (guint) boost::distance(desktop->selection->items());
+ }
+ if (num == 0) {
+ this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to spray."));
+ }
+
+ // Dilating:
+ if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) {
+ sp_spray_dilate(this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false);
+ //this->last_push = motion_doc;
+ this->has_dilated = true;
+
+ // It's slow, so prevent clogging up with events
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ return TRUE;
+ }
+ }
+ break;
+ /* Spray with the scroll */
+ case GDK_SCROLL: {
+ if (event->scroll.state & GDK_BUTTON1_MASK) {
+ double temp ;
+ temp = this->population;
+ this->population = 1.0;
+ desktop->setToolboxAdjustmentValue("population", this->population * 100);
+ Geom::Point const scroll_w(event->button.x, event->button.y);
+ Geom::Point const scroll_dt = desktop->point();;
+
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_SMOOTH: {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+ this->last_push = desktop->dt2doc(scroll_dt);
+ sp_spray_extinput(this, event);
+ desktop->canvas->forceFullRedrawAfterInterruptions(3);
+ this->is_drawing = true;
+ this->is_dilating = true;
+ this->has_dilated = false;
+ if(this->is_dilating && !this->space_panning) {
+ sp_spray_dilate(this, scroll_w, desktop->dt2doc(scroll_dt), Geom::Point(0,0), false);
+ }
+ this->has_dilated = true;
+
+ this->population = temp;
+ desktop->setToolboxAdjustmentValue("population", this->population * 100);
+
+ ret = TRUE;
+ }
+ break;
+ case GDK_SCROLL_RIGHT:
+ {} break;
+ case GDK_SCROLL_LEFT:
+ {} break;
+ }
+ }
+ break;
+ }
+
+ case GDK_BUTTON_RELEASE: {
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ desktop->canvas->endForcedFullRedraws();
+ set_high_motion_precision(false);
+ this->is_drawing = false;
+
+ if (this->is_dilating && event->button.button == 1 && !this->space_panning) {
+ if (!this->has_dilated) {
+ // If we did not rub, do a light tap
+ this->pressure = 0.03;
+ sp_spray_dilate(this, motion_w, desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event));
+ }
+ this->is_dilating = false;
+ this->has_dilated = false;
+ switch (this->mode) {
+ case SPRAY_MODE_COPY:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_SPRAY, _("Spray with copies"));
+ break;
+ case SPRAY_MODE_CLONE:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_SPRAY, _("Spray with clones"));
+ break;
+ case SPRAY_MODE_SINGLE_PATH:
+ object_set.pathUnion(true);
+ desktop->getSelection()->add(object_set.objects().begin(), object_set.objects().end());
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_SPRAY, _("Spray in single path"));
+ break;
+ }
+ }
+ object_set.clear();
+ break;
+ }
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_j:
+ case GDK_KEY_J:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_spray_switch_mode(this, SPRAY_MODE_COPY, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_k:
+ case GDK_KEY_K:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_spray_switch_mode(this, SPRAY_MODE_CLONE, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH
+ case GDK_KEY_l:
+ case GDK_KEY_L:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_spray_switch_mode(this, SPRAY_MODE_SINGLE_PATH, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+#endif
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->population += 0.01;
+ if (this->population > 1.0) {
+ this->population = 1.0;
+ }
+ desktop->setToolboxAdjustmentValue("spray-population", this->population * 100);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->population -= 0.01;
+ if (this->population < 0.0) {
+ this->population = 0.0;
+ }
+ desktop->setToolboxAdjustmentValue("spray-population", this->population * 100);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width += 0.01;
+ if (this->width > 1.0) {
+ this->width = 1.0;
+ }
+ // The same spinbutton is for alt+x
+ desktop->setToolboxAdjustmentValue("spray-width", this->width * 100);
+ sp_spray_update_area(this);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width -= 0.01;
+ if (this->width < 0.01) {
+ this->width = 0.01;
+ }
+ desktop->setToolboxAdjustmentValue("spray-width", this->width * 100);
+ sp_spray_update_area(this);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ this->width = 0.01;
+ desktop->setToolboxAdjustmentValue("spray-width", this->width * 100);
+ sp_spray_update_area(this);
+ ret = TRUE;
+ break;
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ this->width = 1.0;
+ desktop->setToolboxAdjustmentValue("spray-width", this->width * 100);
+ sp_spray_update_area(this);
+ ret = TRUE;
+ break;
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo("spray-width");
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->update_cursor(true);
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ break;
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE: {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->update_cursor(false);
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event));
+ this->message_context->clear();
+ break;
+ default:
+ sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event));
+ break;
+ }
+ }
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+// if ((SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler) {
+// ret = (SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler(event_context, event);
+// }
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+}
+}
+}
+
+/*
+ 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/tools/spray-tool.h b/src/ui/tools/spray-tool.h
new file mode 100644
index 0000000..a4d5d92
--- /dev/null
+++ b/src/ui/tools/spray-tool.h
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_SPRAY_CONTEXT_H__
+#define __SP_SPRAY_CONTEXT_H__
+
+/*
+ * Spray Tool
+ *
+ * Authors:
+ * Pierre-Antoine MARC
+ * Pierre CACLIN
+ * Aurel-Aimé MARMION
+ * Julien LERAY
+ * BenoĂźt LAVORATA
+ * Vincent MONTAGNE
+ * Pierre BARBRY-BLOT
+ * Jabiertxo ARRAIZA
+ * Adrian Boguszewski
+ *
+ * Copyright (C) 2009 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+
+#define SP_SPRAY_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SprayTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_SPRAY_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SprayTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+ namespace UI {
+ namespace Dialog {
+ class Dialog;
+ }
+ }
+}
+
+
+#define SAMPLING_SIZE 8 /* fixme: ?? */
+
+#define TC_MIN_PRESSURE 0.0
+#define TC_MAX_PRESSURE 1.0
+#define TC_DEFAULT_PRESSURE 0.35
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum {
+ SPRAY_MODE_COPY,
+ SPRAY_MODE_CLONE,
+ SPRAY_MODE_SINGLE_PATH,
+ SPRAY_MODE_ERASER,
+ SPRAY_OPTION,
+};
+
+class SprayTool : public ToolBase {
+public:
+ SprayTool();
+ ~SprayTool() override;
+
+ //ToolBase event_context;
+ //Inkscape::UI::Dialog::Dialog *dialog_option;//Attribut de type SprayOptionClass, localisé dans scr/ui/dialog
+ /* extended input data */
+ gdouble pressure;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressurewidth;
+ bool usepressurepopulation;
+ bool usepressurescale;
+ bool usetilt;
+ bool usetext;
+
+ double width;
+ double ratio;
+ double tilt;
+ double rotation_variation;
+ double population;
+ double scale_variation;
+ double scale;
+ double mean;
+ double standard_deviation;
+
+ gint distrib;
+
+ gint mode;
+
+ bool is_drawing;
+
+ bool is_dilating;
+ bool has_dilated;
+ Geom::Point last_push;
+ SPCanvasItem *dilate_area;
+ bool no_overlap;
+ bool picker;
+ bool pick_center;
+ bool pick_inverse_value;
+ bool pick_fill;
+ bool pick_stroke;
+ bool pick_no_overlap;
+ bool over_transparent;
+ bool over_no_transparent;
+ double offset;
+ int pick;
+ bool do_trace;
+ bool pick_to_size;
+ bool pick_to_presence;
+ bool pick_to_color;
+ bool pick_to_opacity;
+ bool invert_picked;
+ double gamma_picked;
+ double rand_picked;
+ sigc::connection style_set_connection;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ virtual void setCloneTilerPrefs();
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+ void update_cursor(bool /*with_shift*/);
+
+ ObjectSet* objectSet() {
+ return &object_set;
+ }
+
+private:
+ ObjectSet object_set;
+};
+
+}
+}
+}
+
+#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/tools/star-tool.cpp b/src/ui/tools/star-tool.cpp
new file mode 100644
index 0000000..8b916ae
--- /dev/null
+++ b/src/ui/tools/star-tool.cpp
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Star drawing context
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ * Copyright (C) 2001-2002 Mitsuru Oka
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-item.h"
+
+#include "include/macros.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-star.h"
+
+#include "ui/pixmaps/cursor-star.xpm"
+
+#include "ui/shape-editor.h"
+#include "ui/tools/star-tool.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& StarTool::getPrefsPath() {
+ return StarTool::prefsPath;
+}
+
+const std::string StarTool::prefsPath = "/tools/shapes/star";
+
+StarTool::StarTool()
+ : ToolBase(cursor_star_xpm)
+ , star(nullptr)
+ , magnitude(5)
+ , proportion(0.5)
+ , isflatsided(false)
+ , rounded(0)
+ , randomized(0)
+{
+}
+
+void StarTool::finish() {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ ToolBase::finish();
+}
+
+StarTool::~StarTool() {
+ this->enableGrDrag(false);
+
+ this->sel_changed_connection.disconnect();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ /* fixme: This is necessary because we do not grab */
+ if (this->star) {
+ this->finishItem();
+ }
+}
+
+/**
+ * Callback that processes the "changed" signal on the selection;
+ * destroys old and creates new knotholder.
+ *
+ * @param selection Should not be NULL.
+ */
+void StarTool::selection_changed(Inkscape::Selection* selection) {
+ g_assert (selection != nullptr);
+
+ this->shape_editor->unset_item();
+ this->shape_editor->set_item(selection->singleItem());
+}
+
+void StarTool::setup() {
+ ToolBase::setup();
+
+ sp_event_context_read(this, "magnitude");
+ sp_event_context_read(this, "proportion");
+ sp_event_context_read(this, "isflatsided");
+ sp_event_context_read(this, "rounded");
+ sp_event_context_read(this, "randomized");
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ Inkscape::Selection *selection = this->desktop->getSelection();
+
+ this->sel_changed_connection.disconnect();
+
+ this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &StarTool::selection_changed));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void StarTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "magnitude") {
+ this->magnitude = CLAMP(val.getInt(5), 3, 1024);
+ } else if (path == "proportion") {
+ this->proportion = CLAMP(val.getDouble(0.5), 0.01, 2.0);
+ } else if (path == "isflatsided") {
+ this->isflatsided = val.getBool();
+ } else if (path == "rounded") {
+ this->rounded = val.getDouble();
+ } else if (path == "randomized") {
+ this->randomized = val.getDouble();
+ }
+}
+
+bool StarTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ SPDesktop *desktop = this->desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = true;
+
+ this->center = Inkscape::setup_for_drag_start(desktop, this, event);
+
+ /* Snap center */
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_POINTER_MOTION_HINT_MASK |
+ GDK_BUTTON_PRESS_MASK,
+ nullptr, event->button.time);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ this->drag(motion_dt, event->motion.state);
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ ret = TRUE;
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE));
+ m.unSetup();
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+
+ if (event->button.button == 1 && !this->space_panning) {
+ dragging = false;
+
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the star
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM (desktop->acetate));
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine)
+ case GDK_KEY_Meta_R:
+ sp_event_show_modifier_tip(this->defaultMessageContext(), event,
+ _("<b>Ctrl</b>: snap angle; keep rays radial"),
+ nullptr,
+ nullptr);
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("altx-star");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (dragging) {
+ dragging = false;
+ sp_event_context_discard_delayed_snap_event(this);
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ dragging = false;
+
+ sp_event_context_discard_delayed_snap_event(this);
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the star
+ this->finishItem();
+ }
+ // do not return true, so that space would work switching to selector
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt
+ case GDK_KEY_Meta_R:
+ this->defaultMessageContext()->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void StarTool::drag(Geom::Point p, guint state)
+{
+ SPDesktop *desktop = this->desktop;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ if (!this->star) {
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return;
+ }
+
+ // Create object
+ Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ repr->setAttribute("sodipodi:type", "star");
+
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/star", false);
+
+ this->star = SP_STAR(desktop->currentLayer()->appendChildRepr(repr));
+
+ Inkscape::GC::release(repr);
+ this->star->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
+ this->star->updateRepr();
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(5);
+ }
+
+ /* Snap corner point with no constraints */
+ SnapManager &m = desktop->namedview->snap_manager;
+
+ m.setup(desktop, true, this->star);
+ Geom::Point pt2g = p;
+ m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ Geom::Point const p0 = desktop->dt2doc(this->center);
+ Geom::Point const p1 = desktop->dt2doc(pt2g);
+
+ double const sides = (gdouble) this->magnitude;
+ Geom::Point const d = p1 - p0;
+ Geom::Coord const r1 = Geom::L2(d);
+ double arg1 = atan2(d);
+
+ if (state & GDK_CONTROL_MASK) {
+ /* Snap angle */
+ double snaps_radian = M_PI/snaps;
+ arg1 = std::round(arg1/snaps_radian) * snaps_radian;
+ }
+
+ sp_star_position_set(this->star, this->magnitude, p0, r1, r1 * this->proportion,
+ arg1, arg1 + M_PI / sides, this->isflatsided, this->rounded, this->randomized);
+
+ /* status text */
+ Inkscape::Util::Quantity q = Inkscape::Util::Quantity(r1, "px");
+ Glib::ustring rads = q.string(desktop->namedview->display_units);
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ ( this->isflatsided?
+ _("<b>Polygon</b>: radius %s, angle %.2f&#176;; with <b>Ctrl</b> to snap angle") :
+ _("<b>Star</b>: radius %s, angle %.2f&#176;; with <b>Ctrl</b> to snap angle") ),
+ rads.c_str(), arg1 * 180 / M_PI);
+}
+
+void StarTool::finishItem() {
+ this->message_context->clear();
+
+ if (this->star != nullptr) {
+ if (this->star->r[1] == 0) {
+ // Don't allow the creating of zero sized arc, for example
+ // when the start and and point snap to the snap grid point
+ this->cancel();
+ return;
+ }
+
+ // Set transform center, so that odd stars rotate correctly
+ // LP #462157
+ this->star->setCenter(this->center);
+ this->star->set_shape();
+ this->star->updateRepr(SP_OBJECT_WRITE_EXT);
+ this->star->doWriteTransform(this->star->transform, nullptr, true);
+ desktop->canvas->endForcedFullRedraws();
+
+ desktop->getSelection()->set(this->star);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_STAR,
+ _("Create star"));
+
+ this->star = nullptr;
+ }
+}
+
+void StarTool::cancel() {
+ desktop->getSelection()->clear();
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ if (this->star != nullptr) {
+ this->star->deleteObject();
+ this->star = nullptr;
+ }
+
+ this->within_tolerance = false;
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+
+ desktop->canvas->endForcedFullRedraws();
+
+ DocumentUndo::cancel(desktop->getDocument());
+}
+
+}
+}
+}
+
+/*
+ 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/tools/star-tool.h b/src/ui/tools/star-tool.h
new file mode 100644
index 0000000..08f0d9d
--- /dev/null
+++ b/src/ui/tools/star-tool.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_STAR_CONTEXT_H__
+#define __SP_STAR_CONTEXT_H__
+
+/*
+ * Star drawing context
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ * Copyright (C) 2001-2002 Mitsuru Oka
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+
+class SPStar;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class StarTool : public ToolBase {
+public:
+ StarTool();
+ ~StarTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPStar* star;
+
+ Geom::Point center;
+
+ /* Number of corners */
+ gint magnitude;
+
+ /* Outer/inner radius ratio */
+ gdouble proportion;
+
+ /* flat sides or not? */
+ bool isflatsided;
+
+ /* rounded corners ratio */
+ gdouble rounded;
+
+ // randomization
+ gdouble randomized;
+
+ sigc::connection sel_changed_connection;
+
+ void drag(Geom::Point p, guint state);
+ void finishItem();
+ void cancel();
+ void selection_changed(Inkscape::Selection* selection);
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp
new file mode 100644
index 0000000..5c2e2ca
--- /dev/null
+++ b/src/ui/tools/text-tool.cpp
@@ -0,0 +1,1897 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * TextTool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdkkeysyms.h>
+#include <gtkmm/clipboard.h>
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include <display/sp-canvas.h>
+#include <display/sp-ctrlline.h>
+#include <display/sodipodi-ctrlrect.h>
+#include <display/sp-ctrlquadr.h>
+
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "include/macros.h"
+#include "inkscape.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "text-editing.h"
+#include "verbs.h"
+
+#include "object/sp-flowtext.h"
+#include "object/sp-namedview.h"
+#include "object/sp-text.h"
+#include "object/sp-rect.h"
+#include "object/sp-shape.h"
+#include "object/sp-ellipse.h"
+
+#include "style.h"
+
+#include "ui/pixmaps/cursor-text-insert.xpm"
+#include "ui/pixmaps/cursor-text.xpm"
+
+#include "ui/control-manager.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/text-tool.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-event-vector.h"
+#include "xml/sp-css-attr.h"
+
+using Inkscape::ControlManager;
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void sp_text_context_validate_cursor_iterators(TextTool *tc);
+static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see = true);
+static void sp_text_context_update_text_selection(TextTool *tc);
+static gint sp_text_context_timeout(TextTool *tc);
+static void sp_text_context_forget_text(TextTool *tc);
+
+static gint sptc_focus_in(GtkWidget *widget, GdkEventFocus *event, TextTool *tc);
+static gint sptc_focus_out(GtkWidget *widget, GdkEventFocus *event, TextTool *tc);
+static void sptc_commit(GtkIMContext *imc, gchar *string, TextTool *tc);
+
+const std::string& TextTool::getPrefsPath() {
+ return TextTool::prefsPath;
+}
+
+const std::string TextTool::prefsPath = "/tools/text";
+
+
+TextTool::TextTool()
+ : ToolBase(cursor_text_xpm)
+ , imc(nullptr)
+ , text(nullptr)
+ , pdoc(0, 0)
+ , unimode(false)
+ , unipos(0)
+ , cursor(nullptr)
+ , indicator(nullptr)
+ , frame(nullptr)
+ , timeout(0)
+ , show(false)
+ , phase(false)
+ , nascent_object(false)
+ , over_text(false)
+ , dragging(0)
+ , creating(false)
+ , grabbed(nullptr)
+ , preedit_string(nullptr)
+{
+}
+
+TextTool::~TextTool() {
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ Inkscape::Rubberband::get(this->desktop)->stop();
+}
+
+void TextTool::setup() {
+ GtkSettings* settings = gtk_settings_get_default();
+ gint timeout = 0;
+ g_object_get( settings, "gtk-cursor-blink-time", &timeout, NULL );
+
+ if (timeout < 0) {
+ timeout = 200;
+ } else {
+ timeout /= 2;
+ }
+
+ this->cursor = ControlManager::getManager().createControlLine(desktop->getControls(), Geom::Point(100, 0), Geom::Point(100, 100));
+ this->cursor->setRgba32(0x000000ff);
+ sp_canvas_item_hide(this->cursor);
+
+ // The rectangle box tightly wrapping text object when selected or under cursor.
+ this->indicator = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRLRECT, nullptr);
+ SP_CTRLRECT(this->indicator)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100)));
+ SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0);
+ SP_CTRLRECT(this->indicator)->setShadow(1, 0xffffff7f);
+ sp_canvas_item_hide(this->indicator);
+
+ // The rectangle box outlining wrapping the shape for text in a shape.
+ this->frame = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRLRECT, nullptr);
+ SP_CTRLRECT(this->frame)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100)));
+ SP_CTRLRECT(this->frame)->setColor(0x0000ff7f, false, 0);
+ sp_canvas_item_hide(this->frame);
+
+ this->timeout = g_timeout_add(timeout, (GSourceFunc) sp_text_context_timeout, this);
+
+ this->imc = gtk_im_multicontext_new();
+ if (this->imc) {
+ GtkWidget *canvas = GTK_WIDGET(desktop->getCanvas());
+
+ /* im preedit handling is very broken in inkscape for
+ * multi-byte characters. See bug 1086769.
+ * We need to let the IM handle the preediting, and
+ * just take in the characters when they're finished being
+ * entered.
+ */
+ gtk_im_context_set_use_preedit(this->imc, FALSE);
+ gtk_im_context_set_client_window(this->imc,
+ gtk_widget_get_window (canvas));
+
+ g_signal_connect(G_OBJECT(canvas), "focus_in_event", G_CALLBACK(sptc_focus_in), this);
+ g_signal_connect(G_OBJECT(canvas), "focus_out_event", G_CALLBACK(sptc_focus_out), this);
+ g_signal_connect(G_OBJECT(this->imc), "commit", G_CALLBACK(sptc_commit), this);
+
+ if (gtk_widget_has_focus(canvas)) {
+ sptc_focus_in(canvas, nullptr, this);
+ }
+ }
+
+ ToolBase::setup();
+
+ this->shape_editor = new ShapeEditor(this->desktop);
+
+ SPItem *item = this->desktop->getSelection()->singleItem();
+ if (item && (
+ (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) ||
+ (SP_IS_TEXT(item) && !SP_TEXT(item)->has_shape_inside()) )
+ ) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection = desktop->getSelection()->connectChangedFirst(
+ sigc::mem_fun(*this, &TextTool::_selectionChanged)
+ );
+ this->sel_modified_connection = desktop->getSelection()->connectModifiedFirst(
+ sigc::mem_fun(*this, &TextTool::_selectionModified)
+ );
+ this->style_set_connection = desktop->connectSetStyle(
+ sigc::mem_fun(*this, &TextTool::_styleSet)
+ );
+ this->style_query_connection = desktop->connectQueryStyle(
+ sigc::mem_fun(*this, &TextTool::_styleQueried)
+ );
+
+ _selectionChanged(desktop->getSelection());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/text/selcue")) {
+ this->enableSelectionCue();
+ }
+ if (prefs->getBool("/tools/text/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void TextTool::finish() {
+ if (this->desktop) {
+ sp_signal_disconnect_by_data(this->desktop->getCanvas(), this);
+ }
+
+ this->enableGrDrag(false);
+
+ this->style_set_connection.disconnect();
+ this->style_query_connection.disconnect();
+ this->sel_changed_connection.disconnect();
+ this->sel_modified_connection.disconnect();
+
+ sp_text_context_forget_text(SP_TEXT_CONTEXT(this));
+
+ if (this->imc) {
+ g_object_unref(G_OBJECT(this->imc));
+ this->imc = nullptr;
+ }
+
+ if (this->timeout) {
+ g_source_remove(this->timeout);
+ this->timeout = 0;
+ }
+
+ if (this->cursor) {
+ sp_canvas_item_destroy(this->cursor);
+ this->cursor = nullptr;
+ }
+
+ if (this->indicator) {
+ sp_canvas_item_destroy(this->indicator);
+ this->indicator = nullptr;
+ }
+
+ if (this->frame) {
+ sp_canvas_item_destroy(this->frame);
+ this->frame = nullptr;
+ }
+
+ for (auto & text_selection_quad : this->text_selection_quads) {
+ sp_canvas_item_hide(text_selection_quad);
+ sp_canvas_item_destroy(text_selection_quad);
+ }
+
+ this->text_selection_quads.clear();
+
+ ToolBase::finish();
+}
+
+bool TextTool::item_handler(SPItem* item, GdkEvent* event) {
+ SPItem *item_ungrouped;
+
+ gint ret = FALSE;
+ sp_text_context_validate_cursor_iterators(this);
+ Inkscape::Text::Layout::iterator old_start = this->text_sel_start;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+ // this var allow too much lees subbselection queries
+ // reducing it to cursor iteracion, mouseup and down
+ // find out clicked item, disregarding groups
+ item_ungrouped = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE);
+ if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
+ desktop->getSelection()->set(item_ungrouped);
+ if (this->text) {
+ // find out click point in document coordinates
+ Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ // set the cursor closest to that point
+ if (event->button.state & GDK_SHIFT_MASK) {
+ this->text_sel_start = old_start;
+ this->text_sel_end = sp_te_get_position_by_coords(this->text, p);
+ } else {
+ this->text_sel_start = this->text_sel_end = sp_te_get_position_by_coords(this->text, p);
+ }
+ // update display
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 1;
+ }
+ ret = TRUE;
+ }
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ if (event->button.button == 1 && this->text && this->dragging) {
+ Inkscape::Text::Layout const *layout = te_get_layout(this->text);
+ if (layout) {
+ if (!layout->isStartOfWord(this->text_sel_start))
+ this->text_sel_start.prevStartOfWord();
+ if (!layout->isEndOfWord(this->text_sel_end))
+ this->text_sel_end.nextEndOfWord();
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 2;
+ ret = TRUE;
+ }
+ }
+ break;
+ case GDK_3BUTTON_PRESS:
+ if (event->button.button == 1 && this->text && this->dragging) {
+ this->text_sel_start.thisStartOfLine();
+ this->text_sel_end.thisEndOfLine();
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 3;
+ ret = TRUE;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && this->dragging && !this->space_panning) {
+ this->dragging = 0;
+ sp_event_context_discard_delayed_snap_event(this);
+ ret = TRUE;
+ desktop->emitToolSubselectionChanged((gpointer)this);
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::item_handler(item, event);
+ }
+
+ return ret;
+}
+
+static void sp_text_context_setup_text(TextTool *tc)
+{
+ ToolBase *ec = SP_EVENT_CONTEXT(tc);
+
+ /* Create <text> */
+ Inkscape::XML::Document *xml_doc = ec->desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text");
+ rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
+
+ /* Set style */
+ sp_desktop_apply_style_tool(ec->desktop, rtext, "/tools/text", true);
+
+ sp_repr_set_svg_double(rtext, "x", tc->pdoc[Geom::X]);
+ sp_repr_set_svg_double(rtext, "y", tc->pdoc[Geom::Y]);
+
+ /* Create <tspan> */
+ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
+ rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
+ rtext->addChild(rtspan, nullptr);
+ Inkscape::GC::release(rtspan);
+
+ /* Create TEXT */
+ Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
+ rtspan->addChild(rstring, nullptr);
+ Inkscape::GC::release(rstring);
+ SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext));
+ /* fixme: Is selection::changed really immediate? */
+ /* yes, it's immediate .. why does it matter? */
+ ec->desktop->getSelection()->set(text_item);
+ Inkscape::GC::release(rtext);
+ text_item->transform = SP_ITEM(ec->desktop->currentLayer())->i2doc_affine().inverse();
+
+ text_item->updateRepr();
+ text_item->doWriteTransform(text_item->transform, nullptr, true);
+ DocumentUndo::done(ec->desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Create text"));
+}
+
+/**
+ * Insert the character indicated by tc.uni to replace the current selection,
+ * and reset tc.uni/tc.unipos to empty string.
+ *
+ * \pre tc.uni/tc.unipos non-empty.
+ */
+static void insert_uni_char(TextTool *const tc)
+{
+ g_return_if_fail(tc->unipos
+ && tc->unipos < sizeof(tc->uni)
+ && tc->uni[tc->unipos] == '\0');
+ unsigned int uv;
+ std::stringstream ss;
+ ss << std::hex << tc->uni;
+ ss >> uv;
+ tc->unipos = 0;
+ tc->uni[tc->unipos] = '\0';
+
+ if ( !g_unichar_isprint(static_cast<gunichar>(uv))
+ && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) {
+ // This may be due to bad input, so it goes to statusbar.
+ tc->desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE,
+ _("Non-printable character"));
+ } else {
+ if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
+ sp_text_context_setup_text(tc);
+ tc->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+
+ gchar u[10];
+ guint const len = g_unichar_to_utf8(uv, u);
+ u[len] = '\0';
+
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u);
+ sp_text_context_update_cursor(tc);
+ sp_text_context_update_text_selection(tc);
+ DocumentUndo::done(tc->desktop->getDocument(), SP_VERB_DIALOG_TRANSFORM,
+ _("Insert Unicode character"));
+ }
+}
+
+static void hex_to_printable_utf8_buf(char const *const ehex, char *utf8)
+{
+ unsigned int uv;
+ std::stringstream ss;
+ ss << std::hex << ehex;
+ ss >> uv;
+ if (!g_unichar_isprint((gunichar) uv)) {
+ uv = 0xfffd;
+ }
+ guint const len = g_unichar_to_utf8(uv, utf8);
+ utf8[len] = '\0';
+}
+
+static void show_curr_uni_char(TextTool *const tc)
+{
+ g_return_if_fail(tc->unipos < sizeof(tc->uni)
+ && tc->uni[tc->unipos] == '\0');
+ if (tc->unipos) {
+ char utf8[10];
+ hex_to_printable_utf8_buf(tc->uni, utf8);
+
+ /* Status bar messages are in pango markup, so we need xml escaping. */
+ if (utf8[1] == '\0') {
+ switch(utf8[0]) {
+ case '<': strcpy(utf8, "&lt;"); break;
+ case '>': strcpy(utf8, "&gt;"); break;
+ case '&': strcpy(utf8, "&amp;"); break;
+ default: break;
+ }
+ }
+ tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8);
+ } else {
+ tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
+ }
+}
+
+bool TextTool::root_handler(GdkEvent* event) {
+ sp_canvas_item_hide(this->indicator);
+
+ sp_text_context_validate_cursor_iterators(this);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+
+ if (Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false) {
+ return TRUE;
+ }
+
+ // save drag origin
+ this->xp = (gint) event->button.x;
+ this->yp = (gint) event->button.y;
+ this->within_tolerance = true;
+
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ Geom::Point button_dt(desktop->w2d(button_pt));
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ this->p0 = button_dt;
+ Inkscape::Rubberband::get(desktop)->start(desktop, this->p0);
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK,
+ nullptr, event->button.time);
+ this->grabbed = SP_CANVAS_ITEM(desktop->acetate);
+ this->creating = true;
+
+ /* Processed */
+ return TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY: {
+ if (this->creating && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ if ( this->within_tolerance
+ && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance )
+ && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to draw, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ this->within_tolerance = false;
+
+ Geom::Point const motion_pt(event->motion.x, event->motion.y);
+ Geom::Point p = desktop->w2d(motion_pt);
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ Inkscape::Rubberband::get(desktop)->move(p);
+ gobble_motion_events(GDK_BUTTON1_MASK);
+
+ // status text
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::X]), "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::Y]), "px");
+ Glib::ustring xs = x_q.string(desktop->namedview->display_units);
+ Glib::ustring ys = y_q.string(desktop->namedview->display_units);
+ this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s &#215; %s"), xs.c_str(), ys.c_str());
+ } else if (!this->sp_event_context_knot_mouseover()) {
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE));
+ m.unSetup();
+ }
+ if ((event->motion.state & GDK_BUTTON1_MASK) && this->dragging && !this->space_panning) {
+ Inkscape::Text::Layout const *layout = te_get_layout(this->text);
+ if (!layout)
+ break;
+ // find out click point in document coordinates
+ Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ // set the cursor closest to that point
+ Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(this->text, p);
+ if (this->dragging == 2) {
+ // double-click dragging: go by word
+ if (new_end < this->text_sel_start) {
+ if (!layout->isStartOfWord(new_end))
+ new_end.prevStartOfWord();
+ } else if (!layout->isEndOfWord(new_end))
+ new_end.nextEndOfWord();
+ } else if (this->dragging == 3) {
+ // triple-click dragging: go by line
+ if (new_end < this->text_sel_start)
+ new_end.thisStartOfLine();
+ else
+ new_end.thisEndOfLine();
+ }
+ // update display
+ if (this->text_sel_end != new_end) {
+ this->text_sel_end = new_end;
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ }
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ break;
+ }
+ // find out item under mouse, disregarding groups
+ SPItem *item_ungrouped =
+ desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE, nullptr);
+ if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
+ Inkscape::Text::Layout const *layout = te_get_layout(item_ungrouped);
+ if (layout->inputTruncated()) {
+ SP_CTRLRECT(this->indicator)->setColor(0xff0000ff, false, 0);
+ } else {
+ SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0);
+ }
+ Geom::OptRect ibbox = item_ungrouped->desktopVisualBounds();
+ if (ibbox) {
+ SP_CTRLRECT(this->indicator)->setRectangle(*ibbox);
+ }
+ sp_canvas_item_show(this->indicator);
+
+ this->cursor_shape = cursor_text_insert_xpm;
+ this->sp_event_context_update_cursor();
+ sp_text_context_update_text_selection(this);
+ if (SP_IS_TEXT(item_ungrouped)) {
+ desktop->event_context->defaultMessageContext()->set(
+ Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text."));
+ } else {
+ desktop->event_context->defaultMessageContext()->set(
+ Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text."));
+ }
+ this->over_text = true;
+ } else {
+ this->over_text = false;
+ // update cursor and statusbar: we are not over a text object now
+ this->cursor_shape = cursor_text_xpm;
+ this->sp_event_context_update_cursor();
+ desktop->event_context->defaultMessageContext()->clear();
+ }
+ } break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && !this->space_panning) {
+ sp_event_context_discard_delayed_snap_event(this);
+
+ Geom::Point p1 = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ m.freeSnapReturnByRef(p1, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ Inkscape::Rubberband::get(desktop)->stop();
+
+ if (this->creating && this->within_tolerance) {
+ /* Button 1, set X & Y & new item */
+ desktop->getSelection()->clear();
+ this->pdoc = desktop->dt2doc(p1);
+ this->show = TRUE;
+ this->phase = true;
+ this->nascent_object = true; // new object was just created
+
+ /* Cursor */
+ sp_canvas_item_show(this->cursor);
+ // Cursor height is defined by the new text object's font size; it needs to be set
+ // artificially here, for the text object does not exist yet:
+ double cursor_height = sp_desktop_get_font_size_tool(desktop);
+ auto const y_dir = desktop->yaxisdir();
+ this->cursor->setCoords(p1, p1 - Geom::Point(0, y_dir * cursor_height));
+ if (this->imc) {
+ GdkRectangle im_cursor;
+ Geom::Point const top_left = SP_EVENT_CONTEXT(this)->desktop->get_display_area().corner(3);
+ Geom::Point const cursor_size(0, cursor_height);
+ Geom::Point const im_position = SP_EVENT_CONTEXT(this)->desktop->d2w(p1 + cursor_size - top_left);
+ im_cursor.x = (int) floor(im_position[Geom::X]);
+ im_cursor.y = (int) floor(im_position[Geom::Y]);
+ im_cursor.width = 0;
+ im_cursor.height = (int) -floor(SP_EVENT_CONTEXT(this)->desktop->d2w(cursor_size)[Geom::Y]);
+ gtk_im_context_set_cursor_location(this->imc, &im_cursor);
+ }
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line.")); // FIXME:: this is a copy of a string from _update_cursor below, do not desync
+
+ this->within_tolerance = false;
+ } else if (this->creating) {
+ double cursor_height = sp_desktop_get_font_size_tool(desktop);
+ if (fabs(p1[Geom::Y] - this->p0[Geom::Y]) > cursor_height) {
+ // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance)
+
+ if (prefs->getBool("/tools/text/use_svg2", true)) {
+ // SVG 2 text
+
+ SPItem *text = create_text_with_rectangle (desktop, this->p0, p1);
+
+ desktop->getSelection()->set(text);
+ SPCSSAttr *css = sp_repr_css_attr(text->getRepr(), "style" );
+ sp_repr_css_attr_unref(css);
+
+ } else {
+ // SVG 1.2 text
+
+ SPItem *ft = create_flowtext_with_internal_frame (desktop, this->p0, p1);
+
+ /* Set style */
+ sp_desktop_apply_style_tool(desktop, ft->getRepr(), "/tools/text", true);
+ SPCSSAttr *css = sp_repr_css_attr(ft->getRepr(), "style" );
+ Geom::Affine const local(ft->i2doc_affine());
+ double const ex(local.descrim());
+ if ( (ex != 0.0) && (ex != 1.0) ) {
+ sp_css_attr_scale(css, 1/ex);
+ }
+ ft->setCSS(css,"style");
+ sp_repr_css_attr_unref(css);
+ ft->updateRepr();
+
+ desktop->getSelection()->set(ft);
+ }
+
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created."));
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Create flowed text"));
+
+ } else {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created."));
+ }
+ }
+ this->creating = false;
+ desktop->emitToolSubselectionChanged((gpointer)this);
+ return TRUE;
+ }
+ break;
+ case GDK_KEY_PRESS: {
+ guint const group0_keyval = get_latin_keyval(&event->key);
+
+ if (group0_keyval == GDK_KEY_KP_Add ||
+ group0_keyval == GDK_KEY_KP_Subtract) {
+ if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys
+ break; // otherwise pass on keypad +/- so they can zoom
+ }
+
+ if ((this->text) || (this->nascent_object)) {
+ // there is an active text object in this context, or a new object was just created
+
+ if (this->unimode || !this->imc
+ || (MOD__CTRL(event) && MOD__SHIFT(event)) // input methods tend to steal this for unimode,
+ // but we have our own so make sure they don't swallow it
+ || !gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) {
+ //IM did not consume the key, or we're in unimode
+
+ if (!MOD__CTRL_ONLY(event) && this->unimode) {
+ /* TODO: ISO 14755 (section 3 Definitions) says that we should also
+ accept the first 6 characters of alphabets other than the latin
+ alphabet "if the Latin alphabet is not used". The below is also
+ reasonable (viz. hope that the user's keyboard includes latin
+ characters and force latin interpretation -- just as we do for our
+ keyboard shortcuts), but differs from the ISO 14755
+ recommendation. */
+ switch (group0_keyval) {
+ case GDK_KEY_space:
+ case GDK_KEY_KP_Space: {
+ if (this->unipos) {
+ insert_uni_char(this);
+ }
+ /* Stay in unimode. */
+ show_curr_uni_char(this);
+ return TRUE;
+ }
+
+ case GDK_KEY_BackSpace: {
+ g_return_val_if_fail(this->unipos < sizeof(this->uni), TRUE);
+ if (this->unipos) {
+ this->uni[--this->unipos] = '\0';
+ }
+ show_curr_uni_char(this);
+ return TRUE;
+ }
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ if (this->unipos) {
+ insert_uni_char(this);
+ }
+ /* Exit unimode. */
+ this->unimode = false;
+ this->defaultMessageContext()->clear();
+ return TRUE;
+ }
+
+ case GDK_KEY_Escape: {
+ // Cancel unimode.
+ this->unimode = false;
+ gtk_im_context_reset(this->imc);
+ this->defaultMessageContext()->clear();
+ return TRUE;
+ }
+
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ break;
+
+ default: {
+ if (g_ascii_isxdigit(group0_keyval)) {
+ g_return_val_if_fail(this->unipos < sizeof(this->uni) - 1, TRUE);
+ this->uni[this->unipos++] = group0_keyval;
+ this->uni[this->unipos] = '\0';
+ if (this->unipos == 8) {
+ /* This behaviour is partly to allow us to continue to
+ use a fixed-length buffer for tc->uni. Reason for
+ choosing the number 8 is that it's the length of
+ ``canonical form'' mentioned in the ISO 14755 spec.
+ An advantage over choosing 6 is that it allows using
+ backspace for typos & misremembering when entering a
+ 6-digit number. */
+ insert_uni_char(this);
+ }
+ show_curr_uni_char(this);
+ return TRUE;
+ } else {
+ /* The intent is to ignore but consume characters that could be
+ typos for hex digits. Gtk seems to ignore & consume all
+ non-hex-digits, and we do similar here. Though note that some
+ shortcuts (like keypad +/- for zoom) get processed before
+ reaching this code. */
+ return TRUE;
+ }
+ }
+ }
+ }
+
+ Inkscape::Text::Layout::iterator old_start = this->text_sel_start;
+ Inkscape::Text::Layout::iterator old_end = this->text_sel_end;
+ bool cursor_moved = false;
+ int screenlines = 1;
+ if (this->text) {
+ double spacing = sp_te_get_average_linespacing(this->text);
+ Geom::Rect const d = desktop->get_display_area();
+ screenlines = (int) floor(fabs(d.min()[Geom::Y] - d.max()[Geom::Y])/spacing) - 1;
+ if (screenlines <= 0)
+ screenlines = 1;
+ }
+
+ /* Neither unimode nor IM consumed key; process text tool shortcuts */
+ switch (group0_keyval) {
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("altx-text");
+ return TRUE;
+ }
+ break;
+ case GDK_KEY_space:
+ if (MOD__CTRL_ONLY(event)) {
+ /* No-break space */
+ if (!this->text) { // printable key; create text if none (i.e. if nascent_object)
+ sp_text_context_setup_text(this);
+ this->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+ this->text_sel_start = this->text_sel_end = sp_te_replace(this->text, this->text_sel_start, this->text_sel_end, "\302\240");
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space"));
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Insert no-break space"));
+ return TRUE;
+ }
+ break;
+ case GDK_KEY_U:
+ case GDK_KEY_u:
+ if (MOD__CTRL_ONLY(event) || (MOD__CTRL(event) && MOD__SHIFT(event))) {
+ if (this->unimode) {
+ this->unimode = false;
+ this->defaultMessageContext()->clear();
+ } else {
+ this->unimode = true;
+ this->unipos = 0;
+ this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
+ }
+ if (this->imc) {
+ gtk_im_context_reset(this->imc);
+ }
+ return TRUE;
+ }
+ break;
+ case GDK_KEY_B:
+ case GDK_KEY_b:
+ if (MOD__CTRL_ONLY(event) && this->text) {
+ SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end));
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL
+ || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100
+ || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200
+ || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300
+ || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400)
+ sp_repr_css_set_property(css, "font-weight", "bold");
+ else
+ sp_repr_css_set_property(css, "font-weight", "normal");
+ sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Make bold"));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ break;
+ case GDK_KEY_I:
+ case GDK_KEY_i:
+ if (MOD__CTRL_ONLY(event) && this->text) {
+ SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end));
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (style->font_style.computed != SP_CSS_FONT_STYLE_NORMAL)
+ sp_repr_css_set_property(css, "font-style", "normal");
+ else
+ sp_repr_css_set_property(css, "font-style", "italic");
+ sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Make italic"));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_A:
+ case GDK_KEY_a:
+ if (MOD__CTRL_ONLY(event) && this->text) {
+ Inkscape::Text::Layout const *layout = te_get_layout(this->text);
+ if (layout) {
+ this->text_sel_start = layout->begin();
+ this->text_sel_end = layout->end();
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ if (!this->text) { // printable key; create text if none (i.e. if nascent_object)
+ sp_text_context_setup_text(this);
+ this->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+
+ SPText* text_element = dynamic_cast<SPText*>(text);
+ if (text_element && (text_element->has_shape_inside() || text_element->has_inline_size())) {
+ // Handle new line like any other character.
+ this->text_sel_start = this->text_sel_end = sp_te_insert(this->text, this->text_sel_start, "\n");
+ } else {
+ // Replace new line by either <tspan sodipodi:role="line" or <flowPara>.
+ iterator_pair enter_pair;
+ bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, enter_pair);
+ (void)success; // TODO cleanup
+ this->text_sel_start = this->text_sel_end = enter_pair.first;
+ this->text_sel_start = this->text_sel_end = sp_te_insert_line(this->text, this->text_sel_start);
+ }
+
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("New line"));
+ return TRUE;
+ }
+ case GDK_KEY_BackSpace:
+ if (this->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys
+
+ bool noSelection = false;
+
+ if (MOD__CTRL(event)) {
+ this->text_sel_start = this->text_sel_end;
+ }
+
+ if (this->text_sel_start == this->text_sel_end) {
+ if (MOD__CTRL(event)) {
+ this->text_sel_start.prevStartOfWord();
+ } else {
+ this->text_sel_start.prevCursorPosition();
+ }
+ noSelection = true;
+ }
+
+ iterator_pair bspace_pair;
+ bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, bspace_pair);
+
+ if (noSelection) {
+ if (success) {
+ this->text_sel_start = this->text_sel_end = bspace_pair.first;
+ } else { // nothing deleted
+ this->text_sel_start = this->text_sel_end = bspace_pair.second;
+ }
+ } else {
+ if (success) {
+ this->text_sel_start = this->text_sel_end = bspace_pair.first;
+ } else { // nothing deleted
+ this->text_sel_start = bspace_pair.first;
+ this->text_sel_end = bspace_pair.second;
+ }
+ }
+
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Backspace"));
+ }
+ return TRUE;
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ if (this->text) {
+ bool noSelection = false;
+
+ if (MOD__CTRL(event)) {
+ this->text_sel_start = this->text_sel_end;
+ }
+
+ if (this->text_sel_start == this->text_sel_end) {
+ if (MOD__CTRL(event)) {
+ this->text_sel_end.nextEndOfWord();
+ } else {
+ this->text_sel_end.nextCursorPosition();
+ }
+ noSelection = true;
+ }
+
+ iterator_pair del_pair;
+ bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, del_pair);
+
+ if (noSelection) {
+ this->text_sel_start = this->text_sel_end = del_pair.first;
+ } else {
+ if (success) {
+ this->text_sel_start = this->text_sel_end = del_pair.first;
+ } else { // nothing deleted
+ this->text_sel_start = del_pair.first;
+ this->text_sel_end = del_pair.second;
+ }
+ }
+
+
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Delete"));
+ }
+ return TRUE;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(
+ get_latin_keyval(&event->key), 0); // with any mask
+ if (MOD__SHIFT(event))
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*-10, 0));
+ else
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*-1, 0));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::maybeDone(desktop->getDocument(), "kern:left", SP_VERB_CONTEXT_TEXT, _("Kern to the left"));
+ } else {
+ if (MOD__CTRL(event))
+ this->text_sel_end.cursorLeftWithControl();
+ else
+ this->text_sel_end.cursorLeft();
+ cursor_moved = true;
+ break;
+ }
+ }
+ return TRUE;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(
+ get_latin_keyval(&event->key), 0); // with any mask
+ if (MOD__SHIFT(event))
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*10, 0));
+ else
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*1, 0));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::maybeDone(desktop->getDocument(), "kern:right", SP_VERB_CONTEXT_TEXT, _("Kern to the right"));
+ } else {
+ if (MOD__CTRL(event))
+ this->text_sel_end.cursorRightWithControl();
+ else
+ this->text_sel_end.cursorRight();
+ cursor_moved = true;
+ break;
+ }
+ }
+ return TRUE;
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(
+ get_latin_keyval(&event->key), 0); // with any mask
+ if (MOD__SHIFT(event))
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*-10));
+ else
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*-1));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::maybeDone(desktop->getDocument(), "kern:up", SP_VERB_CONTEXT_TEXT, _("Kern up"));
+ } else {
+ if (MOD__CTRL(event))
+ this->text_sel_end.cursorUpWithControl();
+ else
+ this->text_sel_end.cursorUp();
+ cursor_moved = true;
+ break;
+ }
+ }
+ return TRUE;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(
+ get_latin_keyval(&event->key), 0); // with any mask
+ if (MOD__SHIFT(event))
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*10));
+ else
+ sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*1));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::maybeDone(desktop->getDocument(), "kern:down", SP_VERB_CONTEXT_TEXT, _("Kern down"));
+ } else {
+ if (MOD__CTRL(event))
+ this->text_sel_end.cursorDownWithControl();
+ else
+ this->text_sel_end.cursorDown();
+ cursor_moved = true;
+ break;
+ }
+ }
+ return TRUE;
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ if (this->text) {
+ if (MOD__CTRL(event))
+ this->text_sel_end.thisStartOfShape();
+ else
+ this->text_sel_end.thisStartOfLine();
+ cursor_moved = true;
+ break;
+ }
+ return TRUE;
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ if (this->text) {
+ if (MOD__CTRL(event))
+ this->text_sel_end.nextStartOfShape();
+ else
+ this->text_sel_end.thisEndOfLine();
+ cursor_moved = true;
+ break;
+ }
+ return TRUE;
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ if (this->text) {
+ this->text_sel_end.cursorDown(screenlines);
+ cursor_moved = true;
+ break;
+ }
+ return TRUE;
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ if (this->text) {
+ this->text_sel_end.cursorUp(screenlines);
+ cursor_moved = true;
+ break;
+ }
+ return TRUE;
+ case GDK_KEY_Escape:
+ if (this->creating) {
+ this->creating = false;
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+ Inkscape::Rubberband::get(desktop)->stop();
+ } else {
+ desktop->getSelection()->clear();
+ }
+ this->nascent_object = FALSE;
+ return TRUE;
+ case GDK_KEY_bracketleft:
+ if (this->text) {
+ if (MOD__ALT(event) || MOD__CTRL(event)) {
+ if (MOD__ALT(event)) {
+ if (MOD__SHIFT(event)) {
+ // FIXME: alt+shift+[] does not work, don't know why
+ sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10);
+ } else {
+ sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1);
+ }
+ } else {
+ sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, desktop, -90);
+ }
+ DocumentUndo::maybeDone(desktop->getDocument(), "textrot:ccw", SP_VERB_CONTEXT_TEXT, _("Rotate counterclockwise"));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ }
+ break;
+ case GDK_KEY_bracketright:
+ if (this->text) {
+ if (MOD__ALT(event) || MOD__CTRL(event)) {
+ if (MOD__ALT(event)) {
+ if (MOD__SHIFT(event)) {
+ // FIXME: alt+shift+[] does not work, don't know why
+ sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10);
+ } else {
+ sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1);
+ }
+ } else {
+ sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, desktop, 90);
+ }
+ DocumentUndo::maybeDone(desktop->getDocument(), "textrot:cw", SP_VERB_CONTEXT_TEXT, _("Rotate clockwise"));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ }
+ break;
+ case GDK_KEY_less:
+ case GDK_KEY_comma:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ if (MOD__CTRL(event)) {
+ if (MOD__SHIFT(event))
+ sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10);
+ else
+ sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1);
+ DocumentUndo::maybeDone(desktop->getDocument(), "linespacing:dec", SP_VERB_CONTEXT_TEXT, _("Contract line spacing"));
+ } else {
+ if (MOD__SHIFT(event))
+ sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10);
+ else
+ sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1);
+ DocumentUndo::maybeDone(desktop->getDocument(), "letterspacing:dec", SP_VERB_CONTEXT_TEXT, _("Contract letter spacing"));
+ }
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ }
+ break;
+ case GDK_KEY_greater:
+ case GDK_KEY_period:
+ if (this->text) {
+ if (MOD__ALT(event)) {
+ if (MOD__CTRL(event)) {
+ if (MOD__SHIFT(event))
+ sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10);
+ else
+ sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1);
+ DocumentUndo::maybeDone(desktop->getDocument(), "linespacing:inc", SP_VERB_CONTEXT_TEXT, _("Expand line spacing"));
+ } else {
+ if (MOD__SHIFT(event))
+ sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10);
+ else
+ sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1);
+ DocumentUndo::maybeDone(desktop->getDocument(), "letterspacing:inc", SP_VERB_CONTEXT_TEXT, _("Expand letter spacing"));\
+ }
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return TRUE;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (cursor_moved) {
+ if (!MOD__SHIFT(event))
+ this->text_sel_start = this->text_sel_end;
+ if (old_start != this->text_sel_start || old_end != this->text_sel_end) {
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ }
+ return TRUE;
+ }
+
+ } else return TRUE; // return the "I took care of it" value if it was consumed by the IM
+ } else { // do nothing if there's no object to type in - the key will be sent to parent context,
+ // except up/down that are swallowed to prevent the zoom field from activation
+ if ((group0_keyval == GDK_KEY_Up ||
+ group0_keyval == GDK_KEY_Down ||
+ group0_keyval == GDK_KEY_KP_Up ||
+ group0_keyval == GDK_KEY_KP_Down )
+ && !MOD__CTRL_ONLY(event)) {
+ return TRUE;
+ } else if (group0_keyval == GDK_KEY_Escape) { // cancel rubberband
+ if (this->creating) {
+ this->creating = false;
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+ Inkscape::Rubberband::get(desktop)->stop();
+ }
+ } else if ((group0_keyval == GDK_KEY_x || group0_keyval == GDK_KEY_X) && MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("altx-text");
+ return TRUE;
+ }
+ }
+ break;
+ }
+
+ case GDK_KEY_RELEASE:
+ if (!this->unimode && this->imc && gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) {
+ return TRUE;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // if nobody consumed it so far
+// if ((SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler) { // and there's a handler in parent context,
+// return (SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler(event_context, event); // send event to parent
+// } else {
+// return FALSE; // return "I did nothing" value so that global shortcuts can be activated
+// }
+ return ToolBase::root_handler(event);
+
+}
+
+/**
+ Attempts to paste system clipboard into the currently edited text, returns true on success
+ */
+bool sp_text_paste_inline(ToolBase *ec)
+{
+ if (!SP_IS_TEXT_CONTEXT(ec))
+ return false;
+ TextTool *tc = SP_TEXT_CONTEXT(ec);
+
+ if ((tc->text) || (tc->nascent_object)) {
+ // there is an active text object in this context, or a new object was just created
+
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ Glib::ustring const clip_text = refClipboard->wait_for_text();
+
+ if (!clip_text.empty()) {
+
+ bool is_svg2 = false;
+ SPText *textitem = dynamic_cast<SPText *>(tc->text);
+ if (textitem) {
+ is_svg2 = textitem->has_shape_inside() /*|| textitem->has_inline_size()*/; // Do now since hiding messes this up.
+ textitem->hide_shape_inside();
+ }
+
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text);
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(false);
+ }
+
+ // Fix for 244940
+ // The XML standard defines the following as valid characters
+ // (Extensible Markup Language (XML) 1.0 (Fourth Edition) paragraph 2.2)
+ // char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ // Since what comes in off the paste buffer will go right into XML, clean
+ // the text here.
+ Glib::ustring text(clip_text);
+ Glib::ustring::iterator itr = text.begin();
+ gunichar paste_string_uchar;
+
+ while(itr != text.end())
+ {
+ paste_string_uchar = *itr;
+
+ // Make sure we don't have a control character. We should really check
+ // for the whole range above... Add the rest of the invalid cases from
+ // above if we find additional issues
+ if(paste_string_uchar >= 0x00000020 ||
+ paste_string_uchar == 0x00000009 ||
+ paste_string_uchar == 0x0000000A ||
+ paste_string_uchar == 0x0000000D) {
+ ++itr;
+ } else {
+ itr = text.erase(itr);
+ }
+ }
+
+ if (!tc->text) { // create text if none (i.e. if nascent_object)
+ sp_text_context_setup_text(tc);
+ tc->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+
+ // using indices is slow in ustrings. Whatever.
+ Glib::ustring::size_type begin = 0;
+ for ( ; ; ) {
+ Glib::ustring::size_type end = text.find('\n', begin);
+
+ if (end == Glib::ustring::npos || is_svg2) {
+ // Paste everything
+ if (begin != text.length())
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin).c_str());
+ break;
+ }
+
+ // Paste up to new line, add line, repeat.
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin, end - begin).c_str());
+ tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
+ begin = end + 1;
+ }
+ if (textitem) {
+ textitem->show_shape_inside();
+ }
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(true);
+ }
+ DocumentUndo::done(ec->desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Paste text"));
+
+ return true;
+ }
+
+ } // FIXME: else create and select a new object under cursor!
+
+ return false;
+}
+
+/**
+ Gets the raw characters that comprise the currently selected text, converting line
+ breaks into lf characters.
+*/
+Glib::ustring sp_text_get_selected_text(ToolBase const *ec)
+{
+ if (!SP_IS_TEXT_CONTEXT(ec))
+ return "";
+ TextTool const *tc = SP_TEXT_CONTEXT(ec);
+ if (tc->text == nullptr)
+ return "";
+
+ return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end);
+}
+
+SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec)
+{
+ if (!SP_IS_TEXT_CONTEXT(ec))
+ return nullptr;
+ TextTool const *tc = SP_TEXT_CONTEXT(ec);
+ if (tc->text == nullptr)
+ return nullptr;
+
+ SPObject const *obj = sp_te_object_at_position(tc->text, tc->text_sel_end);
+
+ if (obj) {
+ return take_style_from_item(const_cast<SPObject*>(obj));
+ }
+
+ return nullptr;
+}
+// this two functions are commented because are used on clipboard
+// and because slow the text pastinbg and usage a lot
+// and couldent get it working properly we miss font size font style or never work
+// and user usualy want paste as plain text and get the position context
+// style. Anyway I retain for further usage.
+
+/* static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
+{
+ Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList();
+ for ( ; attrs ; attrs++) {
+ gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key));
+ if (other_attr == nullptr || strcmp(attrs->value, other_attr))
+ return false;
+ }
+ attrs = second->attributeList();
+ for ( ; attrs ; attrs++) {
+ gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key));
+ if (other_attr == nullptr || strcmp(attrs->value, other_attr))
+ return false;
+ }
+ return true;
+}
+
+std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned>
+*positions)
+{
+ std::vector<SPCSSAttr*> vec;
+ SPCSSAttr *css, *css_new;
+ TextTool *tc = SP_TEXT_CONTEXT(ec);
+ Inkscape::Text::Layout::iterator i = std::min(tc->text_sel_start, tc->text_sel_end);
+ SPObject const *obj = sp_te_object_at_position(tc->text, i);
+ if (obj) {
+ css = take_style_from_item(const_cast<SPObject*>(obj));
+ }
+ vec.push_back(css);
+ positions->push_back(0);
+ i.nextCharacter();
+ *k = 1;
+ *b = 1;
+ while (i != std::max(tc->text_sel_start, tc->text_sel_end))
+ {
+ obj = sp_te_object_at_position(tc->text, i);
+ if (obj) {
+ css_new = take_style_from_item(const_cast<SPObject*>(obj));
+ }
+ if(!css_attrs_are_equal(css, css_new))
+ {
+ vec.push_back(css_new);
+ css = sp_repr_css_attr_new();
+ sp_repr_css_merge(css, css_new);
+ positions->push_back(*k);
+ (*b)++;
+ }
+ i.nextCharacter();
+ (*k)++;
+ }
+ positions->push_back(*k);
+ return vec;
+}
+ */
+
+/**
+ Deletes the currently selected characters. Returns false if there is no
+ text selection currently.
+*/
+bool sp_text_delete_selection(ToolBase *ec)
+{
+ if (!SP_IS_TEXT_CONTEXT(ec))
+ return false;
+ TextTool *tc = SP_TEXT_CONTEXT(ec);
+ if (tc->text == nullptr)
+ return false;
+
+ if (tc->text_sel_start == tc->text_sel_end)
+ return false;
+
+ iterator_pair pair;
+ bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair);
+
+
+ if (success) {
+ tc->text_sel_start = tc->text_sel_end = pair.first;
+ } else { // nothing deleted
+ tc->text_sel_start = pair.first;
+ tc->text_sel_end = pair.second;
+ }
+
+ sp_text_context_update_cursor(tc);
+ sp_text_context_update_text_selection(tc);
+
+ return true;
+}
+
+/**
+ * \param selection Should not be NULL.
+ */
+void TextTool::_selectionChanged(Inkscape::Selection *selection)
+{
+ g_assert(selection != nullptr);
+
+ ToolBase *ec = SP_EVENT_CONTEXT(this);
+
+ ec->shape_editor->unset_item();
+ SPItem *item = selection->singleItem();
+ if (item && (
+ (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) ||
+ (SP_IS_TEXT(item) &&
+ !(SP_TEXT(item)->has_shape_inside() && !SP_TEXT(item)->get_first_rectangle()))
+ )) {
+ ec->shape_editor->set_item(item);
+ }
+
+ if (this->text && (item != this->text)) {
+ sp_text_context_forget_text(this);
+ }
+ this->text = nullptr;
+
+ if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
+ this->text = item;
+ Inkscape::Text::Layout const *layout = te_get_layout(this->text);
+ if (layout)
+ this->text_sel_start = this->text_sel_end = layout->end();
+ } else {
+ this->text = nullptr;
+ }
+
+ // we update cursor without scrolling, because this position may not be final;
+ // item_handler moves cusros to the point of click immediately
+ sp_text_context_update_cursor(this, false);
+ sp_text_context_update_text_selection(this);
+}
+
+void TextTool::_selectionModified(Inkscape::Selection */*selection*/, guint /*flags*/)
+{
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+}
+
+bool TextTool::_styleSet(SPCSSAttr const *css)
+{
+ if (this->text == nullptr)
+ return false;
+ if (this->text_sel_start == this->text_sel_end)
+ return false; // will get picked up by the parent and applied to the whole text object
+
+ sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css);
+
+ // This is a bandaid fix... whenever a style is changed it might cause the text layout to
+ // change which requires rewriting the 'x' and 'y' attributes of the tpsans for Inkscape
+ // multi-line text (with sodipodi:role="line"). We need to rewrite the repr after this is
+ // done. rebuldLayout() will be called a second time unnecessarily.
+ SPText* sptext = dynamic_cast<SPText*>(text);
+ if (sptext) {
+ sptext->rebuildLayout();
+ sptext->updateRepr();
+ }
+
+ DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Set text style"));
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ return true;
+}
+
+int TextTool::_styleQueried(SPStyle *style, int property)
+{
+ if (this->text == nullptr) {
+ return QUERY_STYLE_NOTHING;
+ }
+ const Inkscape::Text::Layout *layout = te_get_layout(this->text);
+ if (layout == nullptr) {
+ return QUERY_STYLE_NOTHING;
+ }
+ sp_text_context_validate_cursor_iterators(this);
+
+ std::vector<SPItem*> styles_list;
+
+ Inkscape::Text::Layout::iterator begin_it, end_it;
+ if (this->text_sel_start < this->text_sel_end) {
+ begin_it = this->text_sel_start;
+ end_it = this->text_sel_end;
+ } else {
+ begin_it = this->text_sel_end;
+ end_it = this->text_sel_start;
+ }
+ if (begin_it == end_it) {
+ if (!begin_it.prevCharacter()) {
+ end_it.nextCharacter();
+ }
+ }
+ for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) {
+ SPObject *pos_obj = nullptr;
+ layout->getSourceOfCharacter(it, &pos_obj);
+ if (!pos_obj) {
+ continue;
+ }
+ if (! pos_obj->parent) // the string is not in the document anymore (deleted)
+ return 0;
+
+ if ( SP_IS_STRING(pos_obj) ) {
+ pos_obj = pos_obj->parent; // SPStrings don't have style
+ }
+ styles_list.insert(styles_list.begin(),(SPItem*)pos_obj);
+ }
+
+ int result = sp_desktop_query_style_from_list (styles_list, style, property);
+
+ return result;
+}
+
+static void sp_text_context_validate_cursor_iterators(TextTool *tc)
+{
+ if (tc->text == nullptr)
+ return;
+ Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
+ if (layout) { // undo can change the text length without us knowing it
+ layout->validateIterator(&tc->text_sel_start);
+ layout->validateIterator(&tc->text_sel_end);
+ }
+}
+
+static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see)
+{
+ // due to interruptible display, tc may already be destroyed during a display update before
+ // the cursor update (can't do both atomically, alas)
+ if (!tc->desktop) return;
+
+ if (tc->text) {
+ Geom::Point p0, p1;
+ sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1);
+ Geom::Point const d0 = p0 * tc->text->i2dt_affine();
+ Geom::Point const d1 = p1 * tc->text->i2dt_affine();
+
+ // scroll to show cursor
+ if (scroll_to_see) {
+
+ // We don't want to scroll outside the text box area (i.e. when there is hidden text)
+ // or we could end up in Timbuktu.
+ bool scroll = true;
+ if (SP_IS_TEXT(tc->text)) {
+ Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame();
+ if (opt_frame && (!opt_frame->contains(p0))) {
+ scroll = false;
+ }
+ }
+
+ if (scroll) {
+ Geom::Point const center = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().midpoint();
+ if (Geom::L2(d0 - center) > Geom::L2(d1 - center))
+ // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed
+ SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d0, 1.0);
+ else
+ SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d1, 1.0);
+ }
+ }
+
+ sp_canvas_item_show(tc->cursor);
+ tc->cursor->setCoords(d0, d1);
+
+ /* fixme: ... need another transformation to get canvas widget coordinate space? */
+ if (tc->imc) {
+ GdkRectangle im_cursor = { 0, 0, 1, 1 };
+ Geom::Point const top_left = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().corner(3);
+ Geom::Point const im_d0 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d0 - top_left);
+ Geom::Point const im_d1 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d1 - top_left);
+ im_cursor.x = (int) floor(im_d0[Geom::X]);
+ im_cursor.y = (int) floor(im_d1[Geom::Y]);
+ im_cursor.width = (int) floor(im_d1[Geom::X]) - im_cursor.x;
+ im_cursor.height = (int) floor(im_d0[Geom::Y]) - im_cursor.y;
+ gtk_im_context_set_cursor_location(tc->imc, &im_cursor);
+ }
+
+ tc->show = TRUE;
+ tc->phase = true;
+
+ Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
+ int const nChars = layout->iteratorToCharIndex(layout->end());
+ char const *trunc = "";
+ bool truncated = false;
+ if (layout->inputTruncated()) {
+ truncated = true;
+ trunc = _(" [truncated]");
+ }
+
+ if (truncated) {
+ SP_CTRLRECT(tc->frame)->setColor(0xff0000ff, false, 0);
+ } else {
+ SP_CTRLRECT(tc->frame)->setColor(0x0000ff7f, false, 0);
+ }
+
+ // Frame around text
+ if (SP_IS_FLOWTEXT(tc->text)) {
+ SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (nullptr); // first frame only
+ if (frame) {
+ sp_canvas_item_show(tc->frame);
+ Geom::OptRect frame_bbox = frame->desktopVisualBounds();
+ if (frame_bbox) {
+ SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox);
+ sp_canvas_item_show(tc->frame);
+ }
+ }
+
+ SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_MESSAGE, ngettext("Type or edit flowed text (%d character%s); <b>Enter</b> to start new paragraph.", "Type or edit flowed text (%d characters%s); <b>Enter</b> to start new paragraph.", nChars), nChars, trunc);
+
+ } else if (SP_IS_TEXT(tc->text)) {
+
+ Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame();
+
+ if (opt_frame) {
+ // User units to screen pixels
+ Geom::Rect frame = *opt_frame;
+ frame *= tc->text->i2dt_affine();
+
+ SP_CTRLRECT(tc->frame)->setRectangle(frame);
+ sp_canvas_item_show(tc->frame);
+ } else {
+ sp_canvas_item_hide(tc->frame);
+ }
+
+ } else {
+
+ SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_MESSAGE, ngettext("Type or edit text (%d character%s); <b>Enter</b> to start new line.", "Type or edit text (%d characters%s); <b>Enter</b> to start new line.", nChars), nChars, trunc);
+ }
+
+ } else {
+ sp_canvas_item_hide(tc->cursor);
+ sp_canvas_item_hide(tc->frame);
+ tc->show = FALSE;
+ if (!tc->nascent_object) {
+ SP_EVENT_CONTEXT(tc)->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type.")); // FIXME: this is a copy of string from tools-switch, do not desync
+ }
+ }
+
+ SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)tc);
+}
+
+static void sp_text_context_update_text_selection(TextTool *tc)
+{
+ // due to interruptible display, tc may already be destroyed during a display update before
+ // the selection update (can't do both atomically, alas)
+ if (!tc->desktop) return;
+
+ for (auto & text_selection_quad : tc->text_selection_quads) {
+ sp_canvas_item_hide(text_selection_quad);
+ sp_canvas_item_destroy(text_selection_quad);
+ }
+ tc->text_selection_quads.clear();
+
+ std::vector<Geom::Point> quads;
+ if (tc->text != nullptr)
+ quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, (tc->text)->i2dt_affine());
+ for (unsigned i = 0 ; i < quads.size() ; i += 4) {
+ SPCanvasItem *quad_canvasitem;
+ quad_canvasitem = sp_canvas_item_new(tc->desktop->getControls(), SP_TYPE_CTRLQUADR, nullptr);
+ // FIXME: make the color settable in prefs
+ // for now, use semitrasparent blue, as cairo cannot do inversion :(
+ sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x00777777);
+ sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]);
+ sp_canvas_item_show(quad_canvasitem);
+ tc->text_selection_quads.push_back(quad_canvasitem);
+ }
+
+ if (tc->shape_editor != nullptr) {
+ if (tc->shape_editor->knotholder) {
+ tc->shape_editor->knotholder->update_knots();
+ }
+ }
+}
+
+static gint sp_text_context_timeout(TextTool *tc)
+{
+ if (tc->show) {
+ sp_canvas_item_show(tc->cursor);
+ if (tc->phase) {
+ tc->phase = false;
+ tc->cursor->setRgba32(0x000000ff);
+ } else {
+ tc->phase = true;
+ tc->cursor->setRgba32(0xffffffff);
+ }
+ }
+
+ return TRUE;
+}
+
+static void sp_text_context_forget_text(TextTool *tc)
+{
+ if (! tc->text) return;
+ SPItem *ti = tc->text;
+ (void)ti;
+ /* We have to set it to zero,
+ * or selection changed signal messes everything up */
+ tc->text = nullptr;
+
+/* FIXME: this automatic deletion when nothing is inputted crashes the XML edittor and also crashes when duplicating an empty flowtext.
+ So don't create an empty flowtext in the first place? Create it when first character is typed.
+ */
+/*
+ if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) {
+ Inkscape::XML::Node *text_repr = ti->getRepr();
+ // the repr may already have been unparented
+ // if we were called e.g. as the result of
+ // an undo or the element being removed from
+ // the XML editor
+ if ( text_repr && text_repr->parent() ) {
+ sp_repr_unparent(text_repr);
+ SPDocumentUndo::done(tc->desktop->getDocument(), SP_VERB_CONTEXT_TEXT,
+ _("Remove empty text"));
+ }
+ }
+*/
+}
+
+gint sptc_focus_in(GtkWidget *widget, GdkEventFocus */*event*/, TextTool *tc)
+{
+ gtk_im_context_focus_in(tc->imc);
+ return FALSE;
+}
+
+gint sptc_focus_out(GtkWidget */*widget*/, GdkEventFocus */*event*/, TextTool *tc)
+{
+ gtk_im_context_focus_out(tc->imc);
+ return FALSE;
+}
+
+static void sptc_commit(GtkIMContext */*imc*/, gchar *string, TextTool *tc)
+{
+ if (!tc->text) {
+ sp_text_context_setup_text(tc);
+ tc->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string);
+ sp_text_context_update_cursor(tc);
+ sp_text_context_update_text_selection(tc);
+
+ DocumentUndo::done(tc->text->document, SP_VERB_CONTEXT_TEXT,
+ _("Type text"));
+}
+
+void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where)
+{
+ tc->desktop->selection->set (text);
+ tc->text_sel_start = tc->text_sel_end = where;
+ sp_text_context_update_cursor(tc);
+ sp_text_context_update_text_selection(tc);
+}
+
+void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p)
+{
+ tc->desktop->selection->set (text);
+ sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p));
+}
+
+Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text)
+{
+ if (text != tc->text)
+ return nullptr;
+ return &(tc->text_sel_end);
+}
+
+}
+}
+}
+
+/*
+ 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/tools/text-tool.h b/src/ui/tools/text-tool.h
new file mode 100644
index 0000000..9ea353a
--- /dev/null
+++ b/src/ui/tools/text-tool.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_TEXT_CONTEXT_H__
+#define __SP_TEXT_CONTEXT_H__
+
+/*
+ * TextTool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+#include <2geom/point.h>
+#include "libnrtype/Layout-TNG.h"
+
+#define SP_TEXT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::TextTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_TEXT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::TextTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+typedef struct _GtkIMContext GtkIMContext;
+
+struct SPCtrlLine;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class TextTool : public ToolBase {
+public:
+ TextTool();
+ ~TextTool() override;
+
+ sigc::connection sel_changed_connection;
+ sigc::connection sel_modified_connection;
+ sigc::connection style_set_connection;
+ sigc::connection style_query_connection;
+
+ GtkIMContext *imc;
+
+ SPItem *text; // the text we're editing, or NULL if none selected
+
+ /* Text item position in root coordinates */
+ Geom::Point pdoc;
+ /* Insertion point position */
+ Inkscape::Text::Layout::iterator text_sel_start;
+ Inkscape::Text::Layout::iterator text_sel_end;
+
+ gchar uni[9];
+ bool unimode;
+ guint unipos;
+
+ SPCtrlLine *cursor;
+ SPCanvasItem *indicator;
+ SPCanvasItem *frame; // hiliting the first frame of flowtext; FIXME: make this a list to accommodate arbitrarily many chained shapes
+ std::vector<SPCanvasItem*> text_selection_quads;
+ gint timeout;
+ bool show;
+ bool phase;
+ bool nascent_object; // true if we're clicked on canvas to put cursor, but no text typed yet so ->text is still NULL
+
+ bool over_text; // true if cursor is over a text object
+
+ guint dragging : 2; // dragging selection over text
+ bool creating; // dragging rubberband to create flowtext
+ SPCanvasItem *grabbed; // we grab while we are creating, to get events even if the mouse goes out of the window
+ Geom::Point p0; // initial point if the flowtext rect
+
+ /* Preedit String */
+ gchar* preedit_string;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ void _selectionChanged(Inkscape::Selection *selection);
+ void _selectionModified(Inkscape::Selection *selection, guint flags);
+ bool _styleSet(SPCSSAttr const *css);
+ int _styleQueried(SPStyle *style, int property);
+};
+
+bool sp_text_paste_inline(ToolBase *ec);
+Glib::ustring sp_text_get_selected_text(ToolBase const *ec);
+SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec);
+// std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned>
+// *positions);
+bool sp_text_delete_selection(ToolBase *ec);
+void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where);
+void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p);
+Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text);
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp
new file mode 100644
index 0000000..6dcbe4c
--- /dev/null
+++ b/src/ui/tools/tool-base.cpp
@@ -0,0 +1,1630 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Main event handling, and related helper functions.
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 1999-2012 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm/display.h>
+#include <glibmm/i18n.h>
+
+#include "shortcuts.h"
+#include "file.h"
+
+
+
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "gradient-drag.h"
+#include "knot-ptr.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "rubberband.h"
+#include "selcue.h"
+#include "selection.h"
+#include "sp-cursor.h"
+
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-group.h"
+#include "display/canvas-rotate.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/sp-guide.h"
+
+#include "ui/contextmenu.h"
+#include "ui/interface.h"
+#include "ui/event-debug.h"
+#include "ui/tool/control-point.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/tool-base.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/tool/commit-events.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tool/shape-record.h"
+
+#include "widgets/desktop-widget.h"
+
+#include "xml/node-event-vector.h"
+
+// globals for temporary switching to selector by space
+static bool selector_toggled = FALSE;
+static int switch_selector_to = 0;
+
+// globals for temporary switching to dropper by 'D'
+static bool dropper_toggled = FALSE;
+static int switch_dropper_to = 0;
+
+// globals for keeping track of keyboard scroll events in order to accelerate
+static guint32 scroll_event_time = 0;
+static gdouble scroll_multiply = 1;
+static guint scroll_keyval = 0;
+
+// globals for key processing
+static bool latin_keys_group_valid = FALSE;
+static gint latin_keys_group;
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void set_event_location(SPDesktop * desktop, GdkEvent * event);
+
+
+void ToolBase::set(const Inkscape::Preferences::Entry& /*val*/) {
+}
+
+void ToolBase::finish() {
+ this->desktop->canvas->endForcedFullRedraws();
+ this->enableSelectionCue(false);
+}
+
+SPDesktop const& ToolBase::getDesktop() const {
+ return *desktop;
+}
+
+ToolBase::ToolBase(gchar const *const *cursor_shape, bool uses_snap)
+ : pref_observer(nullptr)
+ , cursor(nullptr)
+ , xp(0)
+ , yp(0)
+ , tolerance(0)
+ , within_tolerance(false)
+ , item_to_select(nullptr)
+ , message_context(nullptr)
+ , _selcue(nullptr)
+ , _grdrag(nullptr)
+ , shape_editor(nullptr)
+ , space_panning(false)
+ , _delayed_snap_event(nullptr)
+ , _dse_callback_in_process(false)
+ , desktop(nullptr)
+ , _uses_snap(uses_snap)
+ , cursor_shape(cursor_shape)
+ , _button1on(false)
+ , _button3on(false)
+{
+}
+
+ToolBase::~ToolBase() {
+ this->message_context = nullptr;
+
+ if (this->desktop) {
+ this->desktop = nullptr;
+ }
+
+ if (this->pref_observer) {
+ delete this->pref_observer;
+ }
+
+ if (this->_delayed_snap_event) {
+ delete this->_delayed_snap_event;
+ }
+}
+
+
+/**
+ * Set the cursor to a standard GDK cursor
+ */
+void ToolBase::sp_event_context_set_cursor(GdkCursorType cursor_type) {
+
+ GtkWidget *w = GTK_WIDGET(this->desktop->getCanvas());
+ GdkDisplay *display = gdk_display_get_default();
+ GdkCursor *cursor = gdk_cursor_new_for_display(display, cursor_type);
+
+ if (cursor) {
+ gdk_window_set_cursor (gtk_widget_get_window (w), cursor);
+ g_object_unref (cursor);
+ }
+}
+
+/**
+ * Recreates and draws cursor on desktop related to ToolBase.
+ */
+void ToolBase::sp_event_context_update_cursor() {
+ Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas()));
+ if (w->get_window()) {
+ if (this->cursor_shape) {
+ bool fillHasColor=false, strokeHasColor=false;
+ guint32 fillColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), true, &fillHasColor);
+ guint32 strokeColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), false, &strokeHasColor);
+ double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), true) : 1.0;
+ double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), false) : 1.0;
+
+ this->cursor = Glib::wrap(sp_cursor_from_xpm(
+ this->cursor_shape,
+ SP_RGBA32_C_COMPOSE(fillColor, fillOpacity),
+ SP_RGBA32_C_COMPOSE(strokeColor, strokeOpacity)
+ ));
+ }
+ w->get_window()->set_cursor(cursor);
+ w->get_display()->flush();
+ }
+ this->desktop->waiting_cursor = false;
+}
+
+/**
+ * Callback that gets called on initialization of ToolBase object.
+ * Redraws mouse cursor, at the moment.
+ *
+ * When you override it, call this method first.
+ */
+void ToolBase::setup() {
+ this->pref_observer = new ToolPrefObserver(this->getPrefsPath(), this);
+ Inkscape::Preferences::get()->addObserver(*(this->pref_observer));
+ this->sp_event_context_update_cursor();
+}
+
+/**
+ * Gobbles next key events on the queue with the same keyval and mask. Returns the number of events consumed.
+ */
+gint gobble_key_events(guint keyval, gint mask) {
+ GdkEvent *event_next;
+ gint i = 0;
+
+ event_next = gdk_event_get();
+ // while the next event is also a key notify with the same keyval and mask,
+ while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type
+ == GDK_KEY_RELEASE) && event_next->key.keyval == keyval && (!mask
+ || (event_next->key.state & mask))) {
+ if (event_next->type == GDK_KEY_PRESS)
+ i++;
+ // kill it
+ gdk_event_free(event_next);
+ // get next
+ event_next = gdk_event_get();
+ }
+ // otherwise, put it back onto the queue
+ if (event_next)
+ gdk_event_put(event_next);
+
+ return i;
+}
+
+/**
+ * Gobbles next motion notify events on the queue with the same mask. Returns the number of events consumed.
+ */
+gint gobble_motion_events(gint mask) {
+ GdkEvent *event_next;
+ gint i = 0;
+
+ event_next = gdk_event_get();
+ // while the next event is also a key notify with the same keyval and mask,
+ while (event_next && event_next->type == GDK_MOTION_NOTIFY
+ && (event_next->motion.state & mask)) {
+ // kill it
+ gdk_event_free(event_next);
+ // get next
+ event_next = gdk_event_get();
+ i++;
+ }
+ // otherwise, put it back onto the queue
+ if (event_next)
+ gdk_event_put(event_next);
+
+ return i;
+}
+
+/**
+ * Toggles current tool between active tool and selector tool.
+ * Subroutine of sp_event_context_private_root_handler().
+ */
+static void sp_toggle_selector(SPDesktop *dt) {
+ if (!dt->event_context)
+ return;
+
+ if (tools_isactive(dt, TOOLS_SELECT)) {
+ if (selector_toggled) {
+ if (switch_selector_to)
+ tools_switch(dt, switch_selector_to);
+ selector_toggled = FALSE;
+ } else
+ return;
+ } else {
+ selector_toggled = TRUE;
+ switch_selector_to = tools_active(dt);
+ tools_switch(dt, TOOLS_SELECT);
+ }
+}
+
+/**
+ * Toggles current tool between active tool and dropper tool.
+ * Subroutine of sp_event_context_private_root_handler().
+ */
+void sp_toggle_dropper(SPDesktop *dt) {
+ if (!dt->event_context)
+ return;
+
+ if (tools_isactive(dt, TOOLS_DROPPER)) {
+ if (dropper_toggled) {
+ if (switch_dropper_to)
+ tools_switch(dt, switch_dropper_to);
+ dropper_toggled = FALSE;
+ } else
+ return;
+ } else {
+ dropper_toggled = TRUE;
+ switch_dropper_to = tools_active(dt);
+ tools_switch(dt, TOOLS_DROPPER);
+ }
+}
+
+/**
+ * Calculates and keeps track of scroll acceleration.
+ * Subroutine of sp_event_context_private_root_handler().
+ */
+static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration,
+ SPCanvas */*canvas*/) {
+ guint32 time_diff = ((GdkEventKey *) event)->time - scroll_event_time;
+
+ /* key pressed within 500ms ? (1/2 second) */
+ if (time_diff > 500 || event->key.keyval != scroll_keyval) {
+ scroll_multiply = 1; // abort acceleration
+ } else {
+ scroll_multiply += acceleration; // continue acceleration
+ }
+
+ scroll_event_time = ((GdkEventKey *) event)->time;
+ scroll_keyval = event->key.keyval;
+
+ return scroll_multiply;
+}
+
+/** Moves the selected points along the supplied unit vector according to
+ * the modifier state of the supplied event. */
+bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
+{
+ if (held_control(event)) return false;
+ unsigned num = 1 + combine_key_events(shortcut_key(event), 0);
+ Geom::Point delta = dir * num;
+ if (held_shift(event)) delta *= 10;
+ if (held_alt(event)) {
+ delta /= desktop->current_zoom();
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px");
+ delta *= nudge;
+ }
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder) {
+ knotholder->transform_selected(Geom::Translate(delta));
+ }
+ } else if (tools_isactive(desktop, TOOLS_NODES)) {
+ Inkscape::UI::Tools::NodeTool *nt = static_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context);
+ if (nt) {
+ for(auto i=nt->_shape_editors.begin();i!=nt->_shape_editors.end();++i){
+ ShapeEditor * shape_editor = i->second;
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder) {
+ knotholder->transform_selected(Geom::Translate(delta));
+ }
+ }
+ }
+ }
+ }
+ return true;
+}
+
+
+bool ToolBase::root_handler(GdkEvent* event) {
+
+ // ui_dump_event (event, "ToolBase::root_handler");
+ static Geom::Point button_w;
+ static unsigned int panning = 0;
+ static unsigned int panning_cursor = 0;
+ static unsigned int zoom_rb = 0;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /// @todo REmove redundant /value in preference keys
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ bool allow_panning = prefs->getBool("/options/spacebarpans/value");
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (panning) {
+ panning = 0;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ ret = TRUE;
+ } else {
+ /* sp_desktop_dialog(); */
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ // save drag origin
+ xp = (gint) event->button.x;
+ yp = (gint) event->button.y;
+ within_tolerance = true;
+
+ button_w = Geom::Point(event->button.x, event->button.y);
+
+ switch (event->button.button) {
+ case 1:
+ if (this->space_panning) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ sp_event_context_discard_delayed_snap_event(this);
+ }
+ panning = 1;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK
+ | GDK_POINTER_MOTION_HINT_MASK, nullptr,
+ event->button.time - 1);
+
+ ret = TRUE;
+ }
+ break;
+
+ case 2:
+ if ((event->button.state & GDK_CONTROL_MASK) && !desktop->get_rotation_lock()) {
+ // On screen canvas rotation preview
+
+ // Grab background before doing anything else
+ sp_canvas_rotate_start (SP_CANVAS_ROTATE(desktop->canvas_rotate),
+ desktop->canvas->_backing_store);
+ sp_canvas_item_ungrab (desktop->acetate);
+ sp_canvas_item_show (desktop->canvas_rotate);
+ sp_canvas_item_grab (desktop->canvas_rotate,
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK,
+ nullptr, event->button.time );
+ // sp_canvas_item_hide (desktop->drawing);
+
+ } else if (event->button.state & GDK_SHIFT_MASK) {
+ zoom_rb = 2;
+ } else {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ sp_event_context_discard_delayed_snap_event(this);
+ }
+ panning = 2;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK |
+ GDK_POINTER_MOTION_HINT_MASK,
+ nullptr, event->button.time - 1);
+
+ }
+
+ ret = TRUE;
+ break;
+
+ case 3:
+ if ((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK)) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ sp_event_context_discard_delayed_snap_event(this);
+ }
+ panning = 3;
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK
+ | GDK_POINTER_MOTION_HINT_MASK, nullptr,
+ event->button.time);
+
+ ret = TRUE;
+ } else {
+ sp_event_root_menu_popup(desktop, nullptr, event);
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (panning) {
+ if (panning == 4 && !xp && !yp ) {
+ // <Space> + mouse panning started, save location and grab canvas
+ xp = event->motion.x;
+ yp = event->motion.y;
+ button_w = Geom::Point(event->motion.x, event->motion.y);
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK
+ | GDK_POINTER_MOTION_HINT_MASK, nullptr,
+ event->motion.time - 1);
+ }
+
+ if ((panning == 2 && !(event->motion.state & GDK_BUTTON2_MASK))
+ || (panning == 1 && !(event->motion.state & GDK_BUTTON1_MASK))
+ || (panning == 3 && !(event->motion.state & GDK_BUTTON3_MASK))) {
+ /* Gdk seems to lose button release for us sometimes :-( */
+ panning = 0;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ ret = TRUE;
+ } else {
+ // To fix https://bugs.launchpad.net/inkscape/+bug/1458200
+ // we increase the tolerance because no sensible data for panning
+ if (within_tolerance && (abs((gint) event->motion.x - xp)
+ < tolerance * 3) && (abs((gint) event->motion.y - yp)
+ < tolerance * 3)) {
+ // do not drag if we're within tolerance from origin
+ break;
+ }
+
+ // Once the user has moved farther than tolerance from
+ // the original location (indicating they intend to move
+ // the object, not click), then always process the motion
+ // notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ // gobble subsequent motion events to prevent "sticking"
+ // when scrolling is slow
+ gobble_motion_events(panning == 2 ? GDK_BUTTON2_MASK : (panning
+ == 1 ? GDK_BUTTON1_MASK : GDK_BUTTON3_MASK));
+
+ if (panning_cursor == 0) {
+ panning_cursor = 1;
+ this->sp_event_context_set_cursor(GDK_FLEUR);
+ }
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const moved_w(motion_w - button_w);
+ this->desktop->scroll_relative(moved_w, true); // we're still scrolling, do not redraw
+ ret = TRUE;
+ }
+ } else if (zoom_rb) {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ if (within_tolerance && (abs((gint) event->motion.x - xp)
+ < tolerance) && (abs((gint) event->motion.y - yp)
+ < tolerance)) {
+ break; // do not drag if we're within tolerance from origin
+ }
+
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ if (Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::Rubberband::get(desktop)->move(motion_dt);
+ } else {
+ Inkscape::Rubberband::get(desktop)->start(desktop, motion_dt);
+ }
+
+ if (zoom_rb == 2) {
+ gobble_motion_events(GDK_BUTTON2_MASK);
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE: {
+ bool middle_mouse_zoom = prefs->getBool("/options/middlemousezoom/value");
+
+ xp = yp = 0;
+
+ if (panning_cursor == 1) {
+ panning_cursor = 0;
+ Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas()));
+ w->get_window()->set_cursor(cursor);
+ }
+
+ if (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) {
+ zoom_rb = 0;
+
+ if (panning) {
+ panning = 0;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+ }
+
+ Geom::Point const event_w(event->button.x, event->button.y);
+ Geom::Point const event_dt(desktop->w2d(event_w));
+
+ double const zoom_inc = prefs->getDoubleLimited(
+ "/options/zoomincrement/value", M_SQRT2, 1.01, 10);
+
+ desktop->zoom_relative_keep_point(event_dt, (event->button.state
+ & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc);
+
+ desktop->updateNow();
+ ret = TRUE;
+ } else if (panning == event->button.button) {
+ panning = 0;
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ // in slow complex drawings, some of the motion events are lost;
+ // to make up for this, we scroll it once again to the button-up event coordinates
+ // (i.e. canvas will always get scrolled all the way to the mouse release point,
+ // even if few intermediate steps were visible)
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const moved_w(motion_w - button_w);
+
+ this->desktop->scroll_relative(moved_w);
+ desktop->updateNow();
+ ret = TRUE;
+ } else if (zoom_rb == event->button.button) {
+ zoom_rb = 0;
+
+ Geom::OptRect const b = Inkscape::Rubberband::get(desktop)->getRectangle();
+ Inkscape::Rubberband::get(desktop)->stop();
+
+ if (b && !within_tolerance) {
+ desktop->set_display_area(*b, 10);
+ }
+
+ ret = TRUE;
+ }
+ }
+ break;
+
+ case GDK_KEY_PRESS: {
+ double const acceleration = prefs->getDoubleLimited(
+ "/options/scrollingacceleration/value", 0, 0, 6);
+ int const key_scroll = prefs->getIntLimited("/options/keyscroll/value",
+ 10, 0, 1000);
+
+ switch (get_latin_keyval(&event->key)) {
+ // GDK insists on stealing these keys (F1 for no idea what, tab for cycling widgets
+ // in the editing window). So we resteal them back and run our regular shortcut
+ // invoker on them.
+ unsigned int shortcut;
+ case GDK_KEY_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ case GDK_KEY_F1:
+ shortcut = sp_shortcut_get_for_event((GdkEventKey*)event);
+ ret = sp_shortcut_invoke(shortcut, desktop);
+ break;
+
+ case GDK_KEY_Q:
+ case GDK_KEY_q:
+ if (desktop->quick_zoomed()) {
+ ret = TRUE;
+ }
+ if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) {
+ desktop->zoom_quick(true);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_W:
+ case GDK_KEY_w:
+ case GDK_KEY_F4:
+ /* Close view */
+ if (MOD__CTRL_ONLY(event)) {
+ sp_ui_close_view(nullptr);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Left: // Ctrl Left
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = (int) floor(key_scroll * accelerate_scroll(event,
+ acceleration, desktop->getCanvas()));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ this->desktop->scroll_relative(Geom::Point(i, 0));
+ ret = TRUE;
+ } else {
+ ret = _keyboardMove(event->key, Geom::Point(-1, 0));
+ }
+ break;
+
+ case GDK_KEY_Up: // Ctrl Up
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = (int) floor(key_scroll * accelerate_scroll(event,
+ acceleration, desktop->getCanvas()));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ this->desktop->scroll_relative(Geom::Point(0, i));
+ ret = TRUE;
+ } else {
+ ret = _keyboardMove(event->key, Geom::Point(0, -desktop->yaxisdir()));
+ }
+ break;
+
+ case GDK_KEY_Right: // Ctrl Right
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = (int) floor(key_scroll * accelerate_scroll(event,
+ acceleration, desktop->getCanvas()));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ this->desktop->scroll_relative(Geom::Point(-i, 0));
+ ret = TRUE;
+ } else {
+ ret = _keyboardMove(event->key, Geom::Point(1, 0));
+ }
+ break;
+
+ case GDK_KEY_Down: // Ctrl Down
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = (int) floor(key_scroll * accelerate_scroll(event,
+ acceleration, desktop->getCanvas()));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ this->desktop->scroll_relative(Geom::Point(0, -i));
+ ret = TRUE;
+ } else {
+ ret = _keyboardMove(event->key, Geom::Point(0, desktop->yaxisdir()));
+ }
+ break;
+
+ case GDK_KEY_Menu:
+ sp_event_root_menu_popup(desktop, nullptr, event);
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_F10:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_event_root_menu_popup(desktop, nullptr, event);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ within_tolerance = true;
+ xp = yp = 0;
+ if (!allow_panning) break;
+ panning = 4;
+ this->space_panning = true;
+ this->message_context->set(Inkscape::INFORMATION_MESSAGE,
+ _("<b>Space+mouse move</b> to pan canvas"));
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->zoom_grab_focus();
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ // Stop panning on any key release
+ if (this->space_panning) {
+ this->space_panning = false;
+ this->message_context->clear();
+ }
+
+ if (panning) {
+ panning = 0;
+ xp = yp = 0;
+
+ sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate));
+
+ desktop->updateNow();
+ }
+
+ if (panning_cursor == 1) {
+ panning_cursor = 0;
+ Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas()));
+ w->get_window()->set_cursor(cursor);
+ }
+
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_space:
+ if (within_tolerance) {
+ // Space was pressed, but not panned
+ sp_toggle_selector(desktop);
+
+ // Be careful, sp_toggle_selector will delete ourselves.
+ // Thus, make sure we return immediately.
+ return true;
+ }
+
+ break;
+
+ case GDK_KEY_Q:
+ case GDK_KEY_q:
+ if (desktop->quick_zoomed()) {
+ desktop->zoom_quick(false);
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case GDK_SCROLL: {
+ bool ctrl = (event->scroll.state & GDK_CONTROL_MASK);
+ bool shift = (event->scroll.state & GDK_SHIFT_MASK);
+ bool wheelzooms = prefs->getBool("/options/wheelzooms/value");
+
+ int constexpr WHEEL_SCROLL_DEFAULT = 40;
+ int const wheel_scroll = prefs->getIntLimited(
+ "/options/wheelscroll/value", WHEEL_SCROLL_DEFAULT, 0, 1000);
+
+ // Size of smooth-scrolls (only used in GTK+ 3)
+ gdouble delta_x = 0;
+ gdouble delta_y = 0;
+
+ if ((ctrl & shift) && !desktop->get_rotation_lock()) {
+ /* ctrl + shift, rotate */
+
+ double rotate_inc = prefs->getDoubleLimited(
+ "/options/rotateincrement/value", 15, 1, 90, "°" );
+ rotate_inc *= M_PI/180.0;
+
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_UP:
+ // Do nothing
+ break;
+
+ case GDK_SCROLL_DOWN:
+ rotate_inc = -rotate_inc;
+ break;
+
+ case GDK_SCROLL_SMOOTH: {
+ gdk_event_get_scroll_deltas(event, &delta_x, &delta_y);
+#ifdef GDK_WINDOWING_QUARTZ
+ // MacBook trackpad scroll event gives pixel delta
+ delta_y /= WHEEL_SCROLL_DEFAULT;
+#endif
+ double delta_y_clamped = CLAMP(delta_y, -1.0, 1.0); // values > 1 result in excessive rotating
+ rotate_inc = rotate_inc * -delta_y_clamped;
+ break;
+ }
+
+ default:
+ rotate_inc = 0.0;
+ break;
+ }
+
+ if (rotate_inc != 0.0) {
+ Geom::Point const scroll_dt = desktop->point();
+ desktop->rotate_relative_keep_point(scroll_dt, rotate_inc);
+ }
+
+ } else if (shift && !ctrl) {
+ /* shift + wheel, pan left--right */
+
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_LEFT:
+ desktop->scroll_relative(Geom::Point(wheel_scroll, 0));
+ break;
+
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_RIGHT:
+ desktop->scroll_relative(Geom::Point(-wheel_scroll, 0));
+ break;
+
+ case GDK_SCROLL_SMOOTH: {
+ gdk_event_get_scroll_deltas(event, &delta_x, &delta_y);
+#ifdef GDK_WINDOWING_QUARTZ
+ // MacBook trackpad scroll event gives pixel delta
+ delta_y /= WHEEL_SCROLL_DEFAULT;
+#endif
+ desktop->scroll_relative(Geom::Point(wheel_scroll * -delta_y, 0));
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ } else if ((ctrl && !wheelzooms) || (!ctrl && wheelzooms)) {
+ /* ctrl + wheel, zoom in--out */
+ double rel_zoom;
+ double const zoom_inc = prefs->getDoubleLimited(
+ "/options/zoomincrement/value", M_SQRT2, 1.01, 10);
+
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_UP:
+ rel_zoom = zoom_inc;
+ break;
+
+ case GDK_SCROLL_DOWN:
+ rel_zoom = 1 / zoom_inc;
+ break;
+
+ case GDK_SCROLL_SMOOTH: {
+ gdk_event_get_scroll_deltas(event, &delta_x, &delta_y);
+#ifdef GDK_WINDOWING_QUARTZ
+ // MacBook trackpad scroll event gives pixel delta
+ delta_y /= WHEEL_SCROLL_DEFAULT;
+#endif
+ double delta_y_clamped = CLAMP(std::abs(delta_y), 0.0, 1.0); // values > 1 result in excessive zooming
+ double zoom_inc_scaled = (zoom_inc-1) * delta_y_clamped + 1;
+ if (delta_y < 0) {
+ rel_zoom = zoom_inc_scaled;
+ } else {
+ rel_zoom = 1 / zoom_inc_scaled;
+ }
+ break;
+ }
+
+ default:
+ rel_zoom = 0.0;
+ break;
+ }
+
+ if (rel_zoom != 0.0) {
+ Geom::Point const scroll_dt = desktop->point();
+ desktop->zoom_relative_keep_point(scroll_dt, rel_zoom);
+ }
+
+ /* no modifier, pan up--down (left--right on multiwheel mice?) */
+ } else {
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_UP:
+ desktop->scroll_relative(Geom::Point(0, wheel_scroll));
+ break;
+
+ case GDK_SCROLL_DOWN:
+ desktop->scroll_relative(Geom::Point(0, -wheel_scroll));
+ break;
+
+ case GDK_SCROLL_LEFT:
+ desktop->scroll_relative(Geom::Point(wheel_scroll, 0));
+ break;
+
+ case GDK_SCROLL_RIGHT:
+ desktop->scroll_relative(Geom::Point(-wheel_scroll, 0));
+ break;
+
+ case GDK_SCROLL_SMOOTH:
+ gdk_event_get_scroll_deltas(event, &delta_x, &delta_y);
+#ifdef GDK_WINDOWING_QUARTZ
+ // MacBook trackpad scroll event gives pixel delta
+ delta_x /= WHEEL_SCROLL_DEFAULT;
+ delta_y /= WHEEL_SCROLL_DEFAULT;
+#endif
+ desktop->scroll_relative(Geom::Point(-wheel_scroll*delta_x, -wheel_scroll*delta_y));
+ break;
+ }
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+/**
+ * This function allow to handle global tool events if not _pre function is full overrided.
+ */
+
+bool ToolBase::block_button(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ switch (event->button.button) {
+ case 1:
+ this->_button1on = true;
+ break;
+ case 2:
+ this->_button2on = true;
+ break;
+ case 3:
+ this->_button3on = true;
+ break;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ switch (event->button.button) {
+ case 1:
+ this->_button1on = false;
+ break;
+ case 2:
+ this->_button2on = false;
+ break;
+ case 3:
+ this->_button3on = false;
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if (event->motion.state & Gdk::ModifierType::BUTTON1_MASK) {
+ this->_button1on = true;
+ } else {
+ this->_button1on = false;
+ }
+ if (event->motion.state & Gdk::ModifierType::BUTTON2_MASK) {
+ this->_button2on = true;
+ } else {
+ this->_button2on = false;
+ }
+ if (event->motion.state & Gdk::ModifierType::BUTTON3_MASK) {
+ this->_button3on = true;
+ } else {
+ this->_button3on = false;
+ }
+ }
+ if (this->_button1on == true && this->_button3on == true) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Handles item specific events. Gets called from Gdk.
+ *
+ * Only reacts to right mouse button at the moment.
+ * \todo Fixme: do context sensitive popup menu on items.
+ */
+bool ToolBase::item_handler(SPItem* item, GdkEvent* event) {
+ int ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 3 &&
+ !((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK))) {
+ sp_event_root_menu_popup(this->desktop, item, event);
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+/**
+ * Returns true if we're hovering above a knot (needed because we don't want to pre-snap in that case).
+ */
+bool ToolBase::sp_event_context_knot_mouseover() const {
+ if (this->shape_editor) {
+ return this->shape_editor->knot_mouseover();
+ }
+
+ return false;
+}
+
+/**
+ * Enables/disables the ToolBase's SelCue.
+ */
+void ToolBase::enableSelectionCue(bool enable) {
+ if (enable) {
+ if (!_selcue) {
+ _selcue = new Inkscape::SelCue(desktop);
+ }
+ } else {
+ delete _selcue;
+ _selcue = nullptr;
+ }
+}
+
+/**
+ * Enables/disables the ToolBase's GrDrag.
+ */
+void ToolBase::enableGrDrag(bool enable) {
+ if (enable) {
+ if (!_grdrag) {
+ _grdrag = new GrDrag(desktop);
+ }
+ } else {
+ if (_grdrag) {
+ delete _grdrag;
+ _grdrag = nullptr;
+ }
+ }
+}
+
+/**
+ * Delete a selected GrDrag point
+ */
+bool ToolBase::deleteSelectedDrag(bool just_one) {
+
+ if (_grdrag && !_grdrag->selected.empty()) {
+ _grdrag->deleteSelected(just_one);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/** Enable (or disable) high precision for motion events
+ *
+ * This is intended to be used by drawing tools, that need to process motion events with high accuracy
+ * and high update rate (for example free hand tools)
+ *
+ * With standard accuracy some intermediate motion events might be discarded
+ *
+ * Call this function when an operation that requires high accuracy is started (e.g. mouse button is pressed
+ * to draw a line). Make sure to call it again and restore standard precision afterwards. **/
+void ToolBase::set_high_motion_precision(bool high_precision) {
+ Glib::RefPtr<Gdk::Window> window = desktop->getToplevel()->get_window();
+
+ if (window) {
+ window->set_event_compression(!high_precision);
+ }
+}
+
+/**
+ * Calls virtual set() function of ToolBase.
+ */
+void sp_event_context_read(ToolBase *ec, gchar const *key) {
+ g_return_if_fail(ec != nullptr);
+ g_return_if_fail(SP_IS_EVENT_CONTEXT(ec));
+ g_return_if_fail(key != nullptr);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Inkscape::Preferences::Entry val = prefs->getEntry(ec->pref_observer->observed_path + '/' + key);
+ ec->set(val);
+}
+
+/**
+ * Calls virtual root_handler(), the main event handling function.
+ */
+gint sp_event_context_root_handler(ToolBase * event_context,
+ GdkEvent * event)
+{
+
+ if (!event_context->_uses_snap) {
+ return sp_event_context_virtual_root_handler(event_context, event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ sp_event_context_snap_delay_handler(event_context, nullptr, nullptr,
+ (GdkEventMotion *) event,
+ DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event_context && event_context->_delayed_snap_event) {
+ // If we have any pending snapping action, then invoke it now
+ sp_event_context_snap_watchdog_callback(
+ event_context->_delayed_snap_event);
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ // Snapping will be on hold if we're moving the mouse at high speeds. When starting
+ // drawing a new shape we really should snap though.
+ event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ break;
+ default:
+ break;
+ }
+
+ return sp_event_context_virtual_root_handler(event_context, event);
+}
+
+gint sp_event_context_virtual_root_handler(ToolBase * event_context, GdkEvent * event) {
+ gint ret = false;
+
+ if (event_context) {
+
+ if (event_context->block_button(event)) {
+ return false;
+ }
+ SPDesktop* desktop = event_context->desktop;
+ ret = event_context->root_handler(event);
+
+ set_event_location(desktop, event);
+ }
+
+ return ret;
+}
+
+/**
+ * Calls virtual item_handler(), the item event handling function.
+ */
+gint sp_event_context_item_handler(ToolBase * event_context,
+ SPItem * item, GdkEvent * event)
+{
+ if (!event_context->_uses_snap) {
+ return sp_event_context_virtual_item_handler(event_context, item, event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ sp_event_context_snap_delay_handler(event_context, (gpointer) item, nullptr, (GdkEventMotion *) event, DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event_context && event_context->_delayed_snap_event) {
+ // If we have any pending snapping action, then invoke it now
+ sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event);
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ // Snapping will be on hold if we're moving the mouse at high speeds. When starting
+ // drawing a new shape we really should snap though.
+ event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ break;
+ default:
+ break;
+ }
+
+ return sp_event_context_virtual_item_handler(event_context, item, event);
+}
+
+gint sp_event_context_virtual_item_handler(ToolBase * event_context, SPItem * item, GdkEvent * event) {
+ gint ret = false;
+ if (event_context) { // If no event-context is available then do nothing, otherwise Inkscape would crash
+ // (see the comment in SPDesktop::set_event_context, and bug LP #622350)
+ if (event_context->block_button(event)) {
+ return false;
+ }
+ // et = (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(event_context)))->item_handler(event_context, item, event);
+ ret = event_context->item_handler(item, event);
+
+ if (!ret) {
+ ret = sp_event_context_virtual_root_handler(event_context, event);
+ } else {
+ set_event_location(event_context->desktop, event);
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * Shows coordinates on status bar.
+ */
+static void set_event_location(SPDesktop *desktop, GdkEvent *event) {
+ if (event->type != GDK_MOTION_NOTIFY) {
+ return;
+ }
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+ desktop->set_coordinate_status(button_dt);
+}
+
+//-------------------------------------------------------------------
+/**
+ * Create popup menu and tell Gtk to show it.
+ */
+void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event) {
+
+ // It seems the param item is the SPItem at the bottom of the z-order
+ // Using the same function call used on left click in sp_select_context_item_handler() to get top of z-order
+ // fixme: sp_canvas_arena should set the top z-order object as arena->active
+ item = sp_event_context_find_item (desktop,
+ Geom::Point(event->button.x, event->button.y), FALSE, FALSE);
+
+ if (event->type == GDK_KEY_PRESS && !desktop->getSelection()->isEmpty()) {
+ item = desktop->getSelection()->items().front();
+ }
+
+ ContextMenu* CM = new ContextMenu(desktop, item);
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (window) {
+ if (window->get_style_context()->has_class("dark")) {
+ CM->get_style_context()->add_class("dark");
+ } else {
+ CM->get_style_context()->add_class("bright");
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ CM->get_style_context()->add_class("symbolic");
+ } else {
+ CM->get_style_context()->add_class("regular");
+ }
+ }
+ CM->show();
+
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_KEY_PRESS:
+ CM->popup_at_pointer(event);
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * Show tool context specific modifier tip.
+ */
+void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context,
+ GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip,
+ gchar const *alt_tip) {
+ guint keyval = get_latin_keyval(&event->key);
+
+ bool ctrl = ctrl_tip && (MOD__CTRL(event) || (keyval == GDK_KEY_Control_L) || (keyval
+ == GDK_KEY_Control_R));
+ bool shift = shift_tip && (MOD__SHIFT(event) || (keyval == GDK_KEY_Shift_L) || (keyval
+ == GDK_KEY_Shift_R));
+ bool alt = alt_tip && (MOD__ALT(event) || (keyval == GDK_KEY_Alt_L) || (keyval
+ == GDK_KEY_Alt_R) || (keyval == GDK_KEY_Meta_L) || (keyval == GDK_KEY_Meta_R));
+
+ gchar *tip = g_strdup_printf("%s%s%s%s%s", (ctrl ? ctrl_tip : ""), (ctrl
+ && (shift || alt) ? "; " : ""), (shift ? shift_tip : ""), ((ctrl
+ || shift) && alt ? "; " : ""), (alt ? alt_tip : ""));
+
+ if (strlen(tip) > 0) {
+ message_context->flash(Inkscape::INFORMATION_MESSAGE, tip);
+ }
+
+ g_free(tip);
+}
+
+/**
+ * Try to determine the keys group of Latin layout.
+ * Check available keymap entries for Latin 'a' key and find the minimal integer value.
+ */
+static void update_latin_keys_group() {
+ GdkKeymapKey* keys;
+ gint n_keys;
+
+ latin_keys_group_valid = FALSE;
+ if (gdk_keymap_get_entries_for_keyval(Gdk::Display::get_default()->get_keymap(), GDK_KEY_a, &keys, &n_keys)) {
+ for (gint i = 0; i < n_keys; i++) {
+ if (!latin_keys_group_valid || keys[i].group < latin_keys_group) {
+ latin_keys_group = keys[i].group;
+ latin_keys_group_valid = TRUE;
+ }
+ }
+ g_free(keys);
+ }
+}
+
+/**
+ * Initialize Latin keys group handling.
+ */
+void init_latin_keys_group() {
+ g_signal_connect(G_OBJECT(Gdk::Display::get_default()->get_keymap()),
+ "keys-changed", G_CALLBACK(update_latin_keys_group), NULL);
+ update_latin_keys_group();
+}
+
+/**
+ * Return the keyval corresponding to the key event in Latin group.
+ *
+ * Use this instead of simply event->keyval, so that your keyboard shortcuts
+ * work regardless of layouts (e.g., in Cyrillic).
+ */
+guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers /*= NULL*/) {
+ guint keyval = 0;
+ GdkModifierType modifiers;
+ gint group = latin_keys_group_valid ? latin_keys_group : event->group;
+
+ gdk_keymap_translate_keyboard_state(
+ Gdk::Display::get_default()->get_keymap(),
+ event->hardware_keycode, (GdkModifierType) event->state, group,
+ &keyval, nullptr, nullptr, &modifiers);
+
+ if (consumed_modifiers) {
+#ifndef GDK_WINDOWING_QUARTZ
+ *consumed_modifiers = modifiers;
+#else
+ // gdk_quartz_keymap_translate_keyboard_state fills the `consumed_modifiers`
+ // incorrectly, e.g. assigns 0xB instead of 0x0 when no modifiers pressed.
+
+ *consumed_modifiers = 0;
+
+ for (unsigned mod = 1, statemask = (event->state & GDK_MODIFIER_MASK); mod <= statemask; mod <<= 1) {
+ if ((mod & statemask)) {
+ guint keyval_no_mod = 0;
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), event->hardware_keycode,
+ (GdkModifierType)(event->state & ~mod), group, &keyval_no_mod,
+ nullptr, nullptr, nullptr);
+ if (keyval_no_mod != keyval) {
+ *consumed_modifiers |= mod;
+ }
+ }
+ }
+#endif
+ }
+ return keyval;
+}
+
+/**
+ * Returns item at point p in desktop.
+ *
+ * If state includes alt key mask, cyclically selects under; honors
+ * into_groups.
+ */
+SPItem *sp_event_context_find_item(SPDesktop *desktop, Geom::Point const &p,
+ bool select_under, bool into_groups)
+{
+ SPItem *item = nullptr;
+
+ if (select_under) {
+ auto tmp = desktop->selection->items();
+ std::vector<SPItem *> vec(tmp.begin(), tmp.end());
+ SPItem *selected_at_point = desktop->getItemFromListAtPointBottom(vec, p);
+ item = desktop->getItemAtPoint(p, into_groups, selected_at_point);
+ if (item == nullptr) { // we may have reached bottom, flip over to the top
+ item = desktop->getItemAtPoint(p, into_groups, nullptr);
+ }
+ } else {
+ item = desktop->getItemAtPoint(p, into_groups, nullptr);
+ }
+
+ return item;
+}
+
+/**
+ * Returns item if it is under point p in desktop, at any depth; otherwise returns NULL.
+ *
+ * Honors into_groups.
+ */
+SPItem *
+sp_event_context_over_item(SPDesktop *desktop, SPItem *item,
+ Geom::Point const &p) {
+ std::vector<SPItem*> temp;
+ temp.push_back(item);
+ SPItem *item_at_point = desktop->getItemFromListAtPointBottom(temp, p);
+ return item_at_point;
+}
+
+ShapeEditor *
+sp_event_context_get_shape_editor(ToolBase *ec) {
+ return ec->shape_editor;
+}
+
+
+/**
+ * Analyses the current event, calculates the mouse speed, turns snapping off (temporarily) if the
+ * mouse speed is above a threshold, and stores the current event such that it can be re-triggered when needed
+ * (re-triggering is controlled by a watchdog timer).
+ *
+ * @param ec Pointer to the event context.
+ * @param dse_item Pointer that store a reference to a canvas or to an item.
+ * @param dse_item2 Another pointer, storing a reference to a knot or controlpoint.
+ * @param event Pointer to the motion event.
+ * @param origin Identifier (enum) specifying where the delay (and the call to this method) were initiated.
+ */
+void sp_event_context_snap_delay_handler(ToolBase *ec,
+ gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event,
+ DelayedSnapEvent::DelayedSnapEventOrigin origin)
+{
+ static guint32 prev_time;
+ static boost::optional<Geom::Point> prev_pos;
+
+ if (!ec->_uses_snap || ec->_dse_callback_in_process) {
+ return;
+ }
+
+ // Snapping occurs when dragging with the left mouse button down, or when hovering e.g. in the pen tool with left mouse button up
+ bool const c1 = event->state & GDK_BUTTON2_MASK; // We shouldn't hold back any events when other mouse buttons have been
+ bool const c2 = event->state & GDK_BUTTON3_MASK; // pressed, e.g. when scrolling with the middle mouse button; if we do then
+ // Inkscape will get stuck in an unresponsive state
+ bool const c3 = tools_isactive(ec->desktop, TOOLS_CALLIGRAPHIC);
+ // The snap delay will repeat the last motion event, which will lead to
+ // erroneous points in the calligraphy context. And because we don't snap
+ // in this context, we might just as well disable the snap delay all together
+ bool const c4 = ec->space_panning; // Don't snap while panning with the spacebar
+
+ if (c1 || c2 || c3 || c4) {
+ // Make sure that we don't send any pending snap events to a context if we know in advance
+ // that we're not going to snap any way (e.g. while scrolling with middle mouse button)
+ // Any motion event might affect the state of the context, leading to unexpected behavior
+ sp_event_context_discard_delayed_snap_event(ec);
+ } else if (ec->desktop
+ && ec->desktop->namedview->snap_manager.snapprefs.getSnapEnabledGlobally()) {
+ // Snap when speed drops below e.g. 0.02 px/msec, or when no motion events have occurred for some period.
+ // i.e. snap when we're at stand still. A speed threshold enforces snapping for tablets, which might never
+ // be fully at stand still and might keep spitting out motion events.
+ ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(true); // put snapping on hold
+
+ Geom::Point event_pos(event->x, event->y);
+ guint32 event_t = gdk_event_get_time((GdkEvent *) event);
+
+ if (prev_pos) {
+ Geom::Coord dist = Geom::L2(event_pos - *prev_pos);
+ guint32 delta_t = event_t - prev_time;
+ gdouble speed = delta_t > 0 ? dist / delta_t : 1000;
+ //std::cout << "Mouse speed = " << speed << " px/msec " << std::endl;
+ if (speed > 0.02) { // Jitter threshold, might be needed for tablets
+ // We're moving fast, so postpone any snapping until the next GDK_MOTION_NOTIFY event. We
+ // will keep on postponing the snapping as long as the speed is high.
+ // We must snap at some point in time though, so set a watchdog timer at some time from
+ // now, just in case there's no future motion event that drops under the speed limit (when
+ // stopping abruptly)
+ delete ec->_delayed_snap_event;
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); // watchdog is reset, i.e. pushed forward in time
+ // If the watchdog expires before a new motion event is received, we will snap (as explained
+ // above). This means however that when the timer is too short, we will always snap and that the
+ // speed threshold is ineffective. In the extreme case the delay is set to zero, and snapping will
+ // be immediate, as it used to be in the old days ;-).
+ } else { // Speed is very low, so we're virtually at stand still
+ // But if we're really standing still, then we should snap now. We could use some low-pass filtering,
+ // otherwise snapping occurs for each jitter movement. For this filtering we'll leave the watchdog to expire,
+ // snap, and set a new watchdog again.
+ if (ec->_delayed_snap_event == nullptr) { // no watchdog has been set
+ // it might have already expired, so we'll set a new one; the snapping frequency will be limited this way
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin);
+ } // else: watchdog has been set before and we'll wait for it to expire
+ }
+ } else {
+ // This is the first GDK_MOTION_NOTIFY event, so postpone snapping and set the watchdog
+ g_assert(ec->_delayed_snap_event == nullptr);
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin);
+ }
+
+ prev_pos = event_pos;
+ prev_time = event_t;
+ }
+}
+
+/**
+ * When the snap delay watchdog timer barks, this method will be called and will re-inject the last motion
+ * event in an appropriate place, with snapping being turned on again.
+ */
+gboolean sp_event_context_snap_watchdog_callback(gpointer data) {
+ // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated
+ DelayedSnapEvent *dse = reinterpret_cast<DelayedSnapEvent*> (data);
+
+ if (dse == nullptr) {
+ // This might occur when this method is called directly, i.e. not through the timer
+ // E.g. on GDK_BUTTON_RELEASE in sp_event_context_root_handler()
+ return FALSE;
+ }
+
+ ToolBase *ec = dse->getEventContext();
+ if (ec == nullptr) {
+ delete dse;
+ return false;
+ }
+ if (ec->desktop == nullptr) {
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+ return false;
+ }
+
+ ec->_dse_callback_in_process = true;
+
+ SPDesktop *dt = ec->desktop;
+ dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+
+ // Depending on where the delayed snap event originated from, we will inject it back at it's origin
+ // The switch below takes care of that and prepares the relevant parameters
+ switch (dse->getOrigin()) {
+ case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER:
+ sp_event_context_virtual_root_handler(ec, dse->getEvent());
+ break;
+ case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: {
+ gpointer item = dse->getItem();
+ if (item && SP_IS_ITEM(item)) {
+ sp_event_context_virtual_item_handler(ec, SP_ITEM(item), dse->getEvent());
+ }
+ }
+ break;
+ case DelayedSnapEvent::KNOT_HANDLER: {
+ gpointer knot = dse->getItem2();
+ check_if_knot_deleted(knot);
+ if (knot && SP_IS_KNOT(knot)) {
+ sp_knot_handler_request_position(dse->getEvent(), SP_KNOT(knot));
+ }
+ }
+ break;
+ case DelayedSnapEvent::CONTROL_POINT_HANDLER: {
+ using Inkscape::UI::ControlPoint;
+ gpointer pitem2 = dse->getItem2();
+ if (!pitem2)
+ {
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+ return false;
+ }
+ ControlPoint *point = reinterpret_cast<ControlPoint*> (pitem2);
+ if (point) {
+ if (point->position().isFinite() && (dt == point->_desktop)) {
+ point->_eventHandler(ec, dse->getEvent());
+ }
+ else {
+ //workaround:
+ //[Bug 781893] Crash after moving a Bezier node after Knot path effect?
+ // --> at some time, some point with X = 0 and Y = nan (not a number) is created ...
+ // even so, the desktop pointer is invalid and equal to 0xff
+ g_warning ("encountered non finite point when evaluating snapping callback");
+ }
+ }
+ }
+ break;
+ case DelayedSnapEvent::GUIDE_HANDLER: {
+ gpointer item = dse->getItem();
+ gpointer item2 = dse->getItem2();
+ if (item && item2) {
+ g_assert(SP_IS_CANVAS_ITEM(item));
+ g_assert(SP_IS_GUIDE(item2));
+ sp_dt_guide_event(SP_CANVAS_ITEM(item), dse->getEvent(), item2);
+ }
+ }
+ break;
+ case DelayedSnapEvent::GUIDE_HRULER:
+ case DelayedSnapEvent::GUIDE_VRULER: {
+ gpointer item = dse->getItem();
+ gpointer item2 = dse->getItem2();
+ if (item && item2) {
+ g_assert(GTK_IS_WIDGET(item));
+ g_assert(SP_IS_DESKTOP_WIDGET(item2));
+ if (dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER) {
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), true);
+ } else {
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), false);
+ }
+ }
+ }
+ break;
+ default:
+ g_warning("Origin of snap-delay event has not been defined!;");
+ break;
+ }
+
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+
+ ec->_dse_callback_in_process = false;
+
+ return FALSE; //Kills the timer and stops it from executing this callback over and over again.
+}
+
+void sp_event_context_discard_delayed_snap_event(ToolBase *ec) {
+ delete ec->_delayed_snap_event;
+ ec->_delayed_snap_event = nullptr;
+ ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(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/tools/tool-base.h b/src/ui/tools/tool-base.h
new file mode 100644
index 0000000..d3f1000
--- /dev/null
+++ b/src/ui/tools/tool-base.h
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_EVENT_CONTEXT_H
+#define SEEN_SP_EVENT_CONTEXT_H
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ *
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <string>
+#include <memory>
+#include "knot.h"
+#include "knotholder.h"
+#include <2geom/point.h>
+#include <gdk/gdk.h>
+#include <gdkmm/cursor.h>
+#include <glib-object.h>
+#include <sigc++/trackable.h>
+
+#include "preferences.h"
+
+namespace Glib {
+ class ustring;
+}
+
+class GrDrag;
+class SPDesktop;
+class SPItem;
+class KnotHolder;
+namespace Inkscape {
+ class MessageContext;
+ class SelCue;
+}
+
+#define SP_EVENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ToolBase*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_EVENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ToolBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+
+
+class ShapeEditor;
+
+namespace Tools {
+
+class ToolBase;
+
+gboolean sp_event_context_snap_watchdog_callback(gpointer data);
+void sp_event_context_discard_delayed_snap_event(ToolBase *ec);
+
+class DelayedSnapEvent {
+public:
+ enum DelayedSnapEventOrigin {
+ UNDEFINED_HANDLER = 0,
+ EVENTCONTEXT_ROOT_HANDLER,
+ EVENTCONTEXT_ITEM_HANDLER,
+ KNOT_HANDLER,
+ CONTROL_POINT_HANDLER,
+ GUIDE_HANDLER,
+ GUIDE_HRULER,
+ GUIDE_VRULER
+ };
+
+ DelayedSnapEvent(ToolBase *event_context, gpointer const dse_item, gpointer dse_item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin const origin)
+ : _timer_id(0)
+ , _event(nullptr)
+ , _item(dse_item)
+ , _item2(dse_item2)
+ , _origin(origin)
+ , _event_context(event_context)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000);
+
+ // We used to have this specified in milliseconds; this has changed to seconds now for consistency's sake
+ if (value > 1) { // Apparently we have an old preference file, this value must have been in milliseconds;
+ value = value / 1000.0; // now convert this value to seconds
+ }
+
+ _timer_id = g_timeout_add(value*1000.0, &sp_event_context_snap_watchdog_callback, this);
+ _event = gdk_event_copy((GdkEvent*) event);
+
+ ((GdkEventMotion *)_event)->time = GDK_CURRENT_TIME;
+ }
+
+ ~DelayedSnapEvent() {
+ if (_timer_id > 0) g_source_remove(_timer_id); // Kill the watchdog
+ if (_event != nullptr) gdk_event_free(_event); // Remove the copy of the original event
+ }
+
+ ToolBase* getEventContext() {
+ return _event_context;
+ }
+
+ DelayedSnapEventOrigin getOrigin() {
+ return _origin;
+ }
+
+ GdkEvent* getEvent() {
+ return _event;
+ }
+
+ gpointer getItem() {
+ return _item;
+ }
+
+ gpointer getItem2() {
+ return _item2;
+ }
+
+private:
+ guint _timer_id;
+ GdkEvent* _event;
+ gpointer _item;
+ gpointer _item2;
+ DelayedSnapEventOrigin _origin;
+ ToolBase* _event_context;
+};
+
+void sp_event_context_snap_delay_handler(ToolBase *ec, gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, DelayedSnapEvent::DelayedSnapEventOrigin origin);
+
+
+/**
+ * Base class for Event processors.
+ *
+ * This is per desktop object, which (its derivatives) implements
+ * different actions bound to mouse events.
+ *
+ * ToolBase is an abstract base class of all tools. As the name
+ * indicates, event context implementations process UI events (mouse
+ * movements and keypresses) and take actions (like creating or modifying
+ * objects). There is one event context implementation for each tool,
+ * plus few abstract base classes. Writing a new tool involves
+ * subclassing ToolBase.
+ */
+class ToolBase
+ : public sigc::trackable
+{
+public:
+ void enableSelectionCue (bool enable=true);
+ void enableGrDrag (bool enable=true);
+ bool deleteSelectedDrag(bool just_one);
+
+ ToolBase(gchar const *const *cursor_shape, bool uses_snap=true);
+
+ virtual ~ToolBase();
+
+ ToolBase(const ToolBase&) = delete;
+ ToolBase& operator=(const ToolBase&) = delete;
+
+ Inkscape::Preferences::Observer *pref_observer;
+ Glib::RefPtr<Gdk::Cursor> cursor;
+
+ gint xp, yp; ///< where drag started
+ gint tolerance;
+ bool _button1on;
+ bool _button2on;
+ bool _button3on;
+ bool within_tolerance; ///< are we still within tolerance of origin
+
+ SPItem *item_to_select; ///< the item where mouse_press occurred, to
+ ///< be selected if this is a click not drag
+
+ Inkscape::MessageContext *defaultMessageContext() const {
+ return message_context.get();
+ }
+
+ std::unique_ptr<Inkscape::MessageContext> message_context;
+
+ Inkscape::SelCue *_selcue;
+
+ GrDrag *_grdrag;
+
+ GrDrag *get_drag () {
+ return _grdrag;
+ }
+
+ ShapeEditor* shape_editor;
+
+ bool space_panning;
+ bool rotating_mode;
+
+ DelayedSnapEvent *_delayed_snap_event;
+ bool _dse_callback_in_process;
+
+ virtual void setup();
+ virtual void finish();
+
+ // Is called by our pref_observer if a preference has been changed.
+ virtual void set(const Inkscape::Preferences::Entry& val);
+ virtual bool root_handler(GdkEvent *event);
+ virtual bool item_handler(SPItem *item, GdkEvent *event);
+ bool block_button(GdkEvent *event);
+
+ virtual const std::string& getPrefsPath() = 0;
+
+ /**
+ * An observer that relays pref changes to the derived classes.
+ */
+ class ToolPrefObserver: public Inkscape::Preferences::Observer {
+ public:
+ ToolPrefObserver(Glib::ustring const &path, ToolBase *ec)
+ : Inkscape::Preferences::Observer(path)
+ , ec(ec)
+ {
+ }
+
+ void notify(Inkscape::Preferences::Entry const &val) override {
+ ec->set(val);
+ }
+
+ private:
+ ToolBase * const ec;
+ };
+
+ SPDesktop const& getDesktop() const;
+
+
+//protected:
+ void sp_event_context_update_cursor();
+
+ SPDesktop *desktop;
+ bool _uses_snap; // TODO: make protected or private
+protected:
+ /// An xpm containing the shape of the tool's cursor.
+ gchar const *const *cursor_shape;
+ bool sp_event_context_knot_mouseover() const;
+
+ void set_high_motion_precision(bool high_precision = true);
+
+private:
+ bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir);
+ void sp_event_context_set_cursor(GdkCursorType cursor_type);
+};
+
+void sp_event_context_read(ToolBase *ec, gchar const *key);
+
+gint sp_event_context_root_handler(ToolBase *ec, GdkEvent *event);
+gint sp_event_context_virtual_root_handler(ToolBase *ec, GdkEvent *event);
+gint sp_event_context_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event);
+gint sp_event_context_virtual_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event);
+
+void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event);
+
+gint gobble_key_events(guint keyval, gint mask);
+gint gobble_motion_events(gint mask);
+
+void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event,
+ gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip);
+
+void init_latin_keys_group();
+guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers = nullptr);
+
+SPItem *sp_event_context_find_item (SPDesktop *desktop, Geom::Point const &p, bool select_under, bool into_groups);
+SPItem *sp_event_context_over_item (SPDesktop *desktop, SPItem *item, Geom::Point const &p);
+
+void sp_toggle_dropper(SPDesktop *dt);
+
+bool sp_event_context_knot_mouseover(ToolBase *ec);
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SP_EVENT_CONTEXT_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/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp
new file mode 100644
index 0000000..0c5e83b
--- /dev/null
+++ b/src/ui/tools/tweak-tool.cpp
@@ -0,0 +1,1535 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * tweaking paths without node editing
+ *
+ * Authors:
+ * bulia byak
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <numeric>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/circle.h>
+
+#include "context-fns.h"
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "include/macros.h"
+#include "message-context.h"
+#include "path-chemistry.h"
+#include "selection.h"
+#include "splivarot.h"
+#include "verbs.h"
+
+#include "display/canvas-arena.h"
+#include "display/curve.h"
+#include "display/sp-canvas.h"
+
+#include "livarot/Shape.h"
+
+#include "object/box3d.h"
+#include "object/filters/gaussian-blur.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-item-transform.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-path.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-stop.h"
+#include "object/sp-text.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-tweak-attract.xpm"
+#include "ui/pixmaps/cursor-tweak-color.xpm"
+#include "ui/pixmaps/cursor-tweak-less.xpm"
+#include "ui/pixmaps/cursor-tweak-more.xpm"
+#include "ui/pixmaps/cursor-tweak-move-in.xpm"
+#include "ui/pixmaps/cursor-tweak-move-jitter.xpm"
+#include "ui/pixmaps/cursor-tweak-move-out.xpm"
+#include "ui/pixmaps/cursor-tweak-move.xpm"
+#include "ui/pixmaps/cursor-tweak-push.xpm"
+#include "ui/pixmaps/cursor-tweak-repel.xpm"
+#include "ui/pixmaps/cursor-tweak-rotate-clockwise.xpm"
+#include "ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm"
+#include "ui/pixmaps/cursor-tweak-roughen.xpm"
+#include "ui/pixmaps/cursor-tweak-scale-down.xpm"
+#include "ui/pixmaps/cursor-tweak-scale-up.xpm"
+#include "ui/pixmaps/cursor-tweak-thicken.xpm"
+#include "ui/pixmaps/cursor-tweak-thin.xpm"
+
+#include "svg/svg.h"
+
+#include "ui/toolbar/tweak-toolbar.h"
+
+#include "ui/tools/tweak-tool.h"
+
+using Inkscape::DocumentUndo;
+
+#define DDC_RED_RGBA 0xff0000ff
+
+#define DYNA_MIN_WIDTH 1.0e-6
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& TweakTool::getPrefsPath() {
+ return TweakTool::prefsPath;
+}
+
+const std::string TweakTool::prefsPath = "/tools/tweak";
+
+TweakTool::TweakTool()
+ : ToolBase(cursor_push_xpm)
+ , pressure(TC_DEFAULT_PRESSURE)
+ , dragging(false)
+ , usepressure(false)
+ , usetilt(false)
+ , width(0.2)
+ , force(0.2)
+ , fidelity(0)
+ , mode(0)
+ , is_drawing(false)
+ , is_dilating(false)
+ , has_dilated(false)
+ , dilate_area(nullptr)
+ , do_h(true)
+ , do_s(true)
+ , do_l(true)
+ , do_o(false)
+{
+}
+
+TweakTool::~TweakTool() {
+ this->enableGrDrag(false);
+
+ this->style_set_connection.disconnect();
+
+ if (this->dilate_area) {
+ sp_canvas_item_destroy(this->dilate_area);
+ this->dilate_area = nullptr;
+ }
+}
+
+static bool is_transform_mode (gint mode)
+{
+ return (mode == TWEAK_MODE_MOVE ||
+ mode == TWEAK_MODE_MOVE_IN_OUT ||
+ mode == TWEAK_MODE_MOVE_JITTER ||
+ mode == TWEAK_MODE_SCALE ||
+ mode == TWEAK_MODE_ROTATE ||
+ mode == TWEAK_MODE_MORELESS);
+}
+
+static bool is_color_mode (gint mode)
+{
+ return (mode == TWEAK_MODE_COLORPAINT || mode == TWEAK_MODE_COLORJITTER || mode == TWEAK_MODE_BLUR);
+}
+
+void TweakTool::update_cursor (bool with_shift) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!desktop->selection->isEmpty()) {
+ num = (guint) boost::distance(desktop->selection->items());
+ sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num);
+ } else {
+ sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected"));
+ }
+
+ switch (this->mode) {
+ case TWEAK_MODE_MOVE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>move</b>."), sel_message);
+ this->cursor_shape = cursor_tweak_move_xpm;
+ break;
+ case TWEAK_MODE_MOVE_IN_OUT:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move in</b>; with Shift to <b>move out</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_tweak_move_out_xpm;
+ } else {
+ this->cursor_shape = cursor_tweak_move_in_xpm;
+ }
+ break;
+ case TWEAK_MODE_MOVE_JITTER:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move randomly</b>."), sel_message);
+ this->cursor_shape = cursor_tweak_move_jitter_xpm;
+ break;
+ case TWEAK_MODE_SCALE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>scale down</b>; with Shift to <b>scale up</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_tweak_scale_up_xpm;
+ } else {
+ this->cursor_shape = cursor_tweak_scale_down_xpm;
+ }
+ break;
+ case TWEAK_MODE_ROTATE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>rotate clockwise</b>; with Shift, <b>counterclockwise</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_tweak_rotate_counterclockwise_xpm;
+ } else {
+ this->cursor_shape = cursor_tweak_rotate_clockwise_xpm;
+ }
+ break;
+ case TWEAK_MODE_MORELESS:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>duplicate</b>; with Shift, <b>delete</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_tweak_less_xpm;
+ } else {
+ this->cursor_shape = cursor_tweak_more_xpm;
+ }
+ break;
+ case TWEAK_MODE_PUSH:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>push paths</b>."), sel_message);
+ this->cursor_shape = cursor_push_xpm;
+ break;
+ case TWEAK_MODE_SHRINK_GROW:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>inset paths</b>; with Shift to <b>outset</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_thicken_xpm;
+ } else {
+ this->cursor_shape = cursor_thin_xpm;
+ }
+ break;
+ case TWEAK_MODE_ATTRACT_REPEL:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>attract paths</b>; with Shift to <b>repel</b>."), sel_message);
+ if (with_shift) {
+ this->cursor_shape = cursor_repel_xpm;
+ } else {
+ this->cursor_shape = cursor_attract_xpm;
+ }
+ break;
+ case TWEAK_MODE_ROUGHEN:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>roughen paths</b>."), sel_message);
+ this->cursor_shape = cursor_roughen_xpm;
+ break;
+ case TWEAK_MODE_COLORPAINT:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>paint objects</b> with color."), sel_message);
+ this->cursor_shape = cursor_color_xpm;
+ break;
+ case TWEAK_MODE_COLORJITTER:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>randomize colors</b>."), sel_message);
+ this->cursor_shape = cursor_color_xpm;
+ break;
+ case TWEAK_MODE_BLUR:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>increase blur</b>; with Shift to <b>decrease</b>."), sel_message);
+ this->cursor_shape = cursor_color_xpm;
+ break;
+ }
+
+ this->sp_event_context_update_cursor();
+ g_free(sel_message);
+}
+
+bool TweakTool::set_style(const SPCSSAttr* css) {
+ if (this->mode == TWEAK_MODE_COLORPAINT) { // intercept color setting only in this mode
+ // we cannot store properties with uris
+ css = sp_css_attr_unset_uris(const_cast<SPCSSAttr *>(css));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setStyle("/tools/tweak/style", const_cast<SPCSSAttr *>(css));
+ return true;
+ }
+
+ return false;
+}
+
+void TweakTool::setup() {
+ ToolBase::setup();
+
+ {
+ /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1));
+
+ SPCurve *c = new SPCurve(path);
+
+ this->dilate_area = sp_canvas_bpath_new(this->desktop->getControls(), c);
+ c->unref();
+ sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0);
+ sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
+ sp_canvas_item_hide(this->dilate_area);
+ }
+
+ this->is_drawing = false;
+
+ sp_event_context_read(this, "width");
+ sp_event_context_read(this, "mode");
+ sp_event_context_read(this, "fidelity");
+ sp_event_context_read(this, "force");
+ sp_event_context_read(this, "usepressure");
+ sp_event_context_read(this, "doh");
+ sp_event_context_read(this, "dol");
+ sp_event_context_read(this, "dos");
+ sp_event_context_read(this, "doo");
+
+ this->style_set_connection = this->desktop->connectSetStyle( // catch style-setting signal in this tool
+ //sigc::bind(sigc::ptr_fun(&sp_tweak_context_style_set), this)
+ sigc::mem_fun(this, &TweakTool::set_style)
+ );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/tweak/selcue")) {
+ this->enableSelectionCue();
+ }
+ if (prefs->getBool("/tools/tweak/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+void TweakTool::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "width") {
+ this->width = CLAMP(val.getDouble(0.1), -1000.0, 1000.0);
+ } else if (path == "mode") {
+ this->mode = val.getInt();
+ this->update_cursor(false);
+ } else if (path == "fidelity") {
+ this->fidelity = CLAMP(val.getDouble(), 0.0, 1.0);
+ } else if (path == "force") {
+ this->force = CLAMP(val.getDouble(1.0), 0, 1.0);
+ } else if (path == "usepressure") {
+ this->usepressure = val.getBool();
+ } else if (path == "doh") {
+ this->do_h = val.getBool();
+ } else if (path == "dos") {
+ this->do_s = val.getBool();
+ } else if (path == "dol") {
+ this->do_l = val.getBool();
+ } else if (path == "doo") {
+ this->do_o = val.getBool();
+ }
+}
+
+static void
+sp_tweak_extinput(TweakTool *tc, GdkEvent *event)
+{
+ if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &tc->pressure)) {
+ tc->pressure = CLAMP (tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE);
+ } else {
+ tc->pressure = TC_DEFAULT_PRESSURE;
+ }
+}
+
+static double
+get_dilate_radius (TweakTool *tc)
+{
+ // 10 times the pen width:
+ return 500 * tc->width/SP_EVENT_CONTEXT(tc)->desktop->current_zoom();
+}
+
+static double
+get_path_force (TweakTool *tc)
+{
+ double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE)
+ /sqrt(SP_EVENT_CONTEXT(tc)->desktop->current_zoom());
+ if (force > 3) {
+ force += 4 * (force - 3);
+ }
+ return force * tc->force;
+}
+
+static double
+get_move_force (TweakTool *tc)
+{
+ double force = (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE);
+ return force * tc->force;
+}
+
+static bool
+sp_tweak_dilate_recursive (Inkscape::Selection *selection, SPItem *item, Geom::Point p, Geom::Point vector, gint mode, double radius, double force, double fidelity, bool reverse)
+{
+ bool did = false;
+
+ {
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box && !is_transform_mode(mode) && !is_color_mode(mode)) {
+ // convert 3D boxes to ordinary groups before tweaking their shapes
+ item = box3d_convert_to_group(box);
+ selection->add(item);
+ }
+ }
+
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ std::vector<SPItem*> items;
+ items.push_back(item);
+ std::vector<SPItem*> selected;
+ std::vector<Inkscape::XML::Node*> to_select;
+ SPDocument *doc = item->document;
+ sp_item_list_to_curves (items, selected, to_select);
+ SPObject* newObj = doc->getObjectByRepr(to_select[0]);
+ item = dynamic_cast<SPItem *>(newObj);
+ g_assert(item != nullptr);
+ selection->add(item);
+ }
+
+ if (dynamic_cast<SPGroup *>(item) && !dynamic_cast<SPBox3D *>(item)) {
+ std::vector<SPItem *> children;
+ for (auto& child: item->children) {
+ if (dynamic_cast<SPItem *>(&child)) {
+ children.push_back(dynamic_cast<SPItem *>(&child));
+ }
+ }
+
+ for (auto i = children.rbegin(); i!= children.rend(); ++i) {
+ SPItem *child = *i;
+ g_assert(child != nullptr);
+ if (sp_tweak_dilate_recursive (selection, child, p, vector, mode, radius, force, fidelity, reverse)) {
+ did = true;
+ }
+ }
+ } else {
+ if (mode == TWEAK_MODE_MOVE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * vector;
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MOVE_IN_OUT) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) *
+ (reverse? (a->midpoint() - p) : (p - a->midpoint()));
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MOVE_JITTER) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double dp = g_random_double_range(0, M_PI*2);
+ double dr = g_random_double_range(0, radius);
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * Geom::Point(cos(dp)*dr, sin(dp)*dr);
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_SCALE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double scale = 1 + (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1);
+ item->scale_rel(Geom::Scale(scale, scale));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_ROTATE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double angle = (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1) * M_PI;
+ angle *= -selection->desktop()->yaxisdir();
+ item->rotate_rel(Geom::Rotate(angle));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MORELESS) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double prob = force * 0.5 * (cos(M_PI * x) + 1);
+ double chance = g_random_double_range(0, 1);
+ if (chance <= prob) {
+ if (reverse) { // delete
+ item->deleteObject(true, true);
+ } else { // duplicate
+ SPDocument *doc = item->document;
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = item->getRepr();
+ SPObject *old_obj = doc->getObjectByRepr(old_repr);
+ Inkscape::XML::Node *parent = old_repr->parent();
+ Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc);
+ parent->appendChild(copy);
+ SPObject *new_obj = doc->getObjectByRepr(copy);
+ if (selection->includes(old_obj)) {
+ selection->add(new_obj);
+ }
+ Inkscape::GC::release(copy);
+ }
+ did = true;
+ }
+ }
+ }
+
+ } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPShape *>(item)) {
+
+ Inkscape::XML::Node *newrepr = nullptr;
+ gint pos = 0;
+ Inkscape::XML::Node *parent = nullptr;
+ char const *id = nullptr;
+ if (!dynamic_cast<SPPath *>(item)) {
+ newrepr = sp_selected_item_to_curved_repr(item, 0);
+ if (!newrepr) {
+ return false;
+ }
+
+ // remember the position of the item
+ pos = item->getRepr()->position();
+ // remember parent
+ parent = item->getRepr()->parent();
+ // remember id
+ id = item->getRepr()->attribute("id");
+ }
+
+ // skip those paths whose bboxes are entirely out of reach with our radius
+ Geom::OptRect bbox = item->documentVisualBounds();
+ if (bbox) {
+ bbox->expandBy(radius);
+ if (!bbox->contains(p)) {
+ return false;
+ }
+ }
+
+ Path *orig = Path_for_item(item, false);
+ if (orig == nullptr) {
+ return false;
+ }
+
+ Path *res = new Path;
+ res->SetBackData(false);
+
+ Shape *theShape = new Shape;
+ Shape *theRes = new Shape;
+ Geom::Affine i2doc(item->i2doc_affine());
+
+ orig->ConvertWithBackData((0.08 - (0.07 * fidelity)) / i2doc.descrim()); // default 0.059
+ orig->Fill(theShape, 0);
+
+ SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style");
+ gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr);
+ if (val && strcmp(val, "nonzero") == 0) {
+ theRes->ConvertToShape(theShape, fill_nonZero);
+ } else if (val && strcmp(val, "evenodd") == 0) {
+ theRes->ConvertToShape(theShape, fill_oddEven);
+ } else {
+ theRes->ConvertToShape(theShape, fill_nonZero);
+ }
+
+ if (Geom::L2(vector) != 0) {
+ vector = 1/Geom::L2(vector) * vector;
+ }
+
+ bool did_this = false;
+ if (mode == TWEAK_MODE_SHRINK_GROW) {
+ if (theShape->MakeTweak(tweak_mode_grow, theRes,
+ reverse? force : -force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0) // 0 means the shape was actually changed
+ did_this = true;
+ } else if (mode == TWEAK_MODE_ATTRACT_REPEL) {
+ if (theShape->MakeTweak(tweak_mode_repel, theRes,
+ reverse? force : -force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0)
+ did_this = true;
+ } else if (mode == TWEAK_MODE_PUSH) {
+ if (theShape->MakeTweak(tweak_mode_push, theRes,
+ 1.0,
+ join_straight, 4.0,
+ true, p, force*2*vector, radius, &i2doc) == 0)
+ did_this = true;
+ } else if (mode == TWEAK_MODE_ROUGHEN) {
+ if (theShape->MakeTweak(tweak_mode_roughen, theRes,
+ force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0)
+ did_this = true;
+ }
+
+ // the rest only makes sense if we actually changed the path
+ if (did_this) {
+ theRes->ConvertToShape(theShape, fill_positive);
+
+ res->Reset();
+ theRes->ConvertToForme(res);
+
+ double th_max = (0.6 - 0.59*sqrt(fidelity)) / i2doc.descrim();
+ double threshold = MAX(th_max, th_max*force);
+ res->ConvertEvenLines(threshold);
+ res->Simplify(threshold / (selection->desktop()->current_zoom()));
+
+ if (newrepr) { // converting to path, need to replace the repr
+ bool is_selected = selection->includes(item);
+ if (is_selected) {
+ selection->remove(item);
+ }
+
+ // It's going to resurrect, so we delete without notifying listeners.
+ item->deleteObject(false);
+
+ // restore id
+ newrepr->setAttribute("id", id);
+ // add the new repr to the parent
+ // move to the saved position
+ parent->addChildAtPos(newrepr, pos);
+
+ if (is_selected)
+ selection->add(newrepr);
+ }
+
+ if (res->descr_cmd.size() > 1) {
+ gchar *str = res->svg_dump_path();
+ if (newrepr) {
+ newrepr->setAttribute("d", str);
+ } else {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ item->setAttribute("inkscape:original-d", str);
+ } else {
+ item->setAttribute("d", str);
+ }
+ }
+ g_free(str);
+ } else {
+ // TODO: if there's 0 or 1 node left, delete this path altogether
+ }
+
+ if (newrepr) {
+ Inkscape::GC::release(newrepr);
+ newrepr = nullptr;
+ }
+ }
+
+ delete theShape;
+ delete theRes;
+ delete orig;
+ delete res;
+
+ if (did_this) {
+ did = true;
+ }
+ }
+
+ }
+
+ return did;
+}
+
+ static void
+tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l)
+{
+ float rgb_g[3];
+
+ if (!do_h || !do_s || !do_l) {
+ float hsl_g[3];
+ SPColor::rgb_to_hsl_floatv (hsl_g, SP_RGBA32_R_F(goal), SP_RGBA32_G_F(goal), SP_RGBA32_B_F(goal));
+ float hsl_c[3];
+ SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]);
+ if (!do_h) {
+ hsl_g[0] = hsl_c[0];
+ }
+ if (!do_s) {
+ hsl_g[1] = hsl_c[1];
+ }
+ if (!do_l) {
+ hsl_g[2] = hsl_c[2];
+ }
+ SPColor::hsl_to_rgb_floatv (rgb_g, hsl_g[0], hsl_g[1], hsl_g[2]);
+ } else {
+ rgb_g[0] = SP_RGBA32_R_F(goal);
+ rgb_g[1] = SP_RGBA32_G_F(goal);
+ rgb_g[2] = SP_RGBA32_B_F(goal);
+ }
+
+ for (int i = 0; i < 3; i++) {
+ double d = rgb_g[i] - color[i];
+ color[i] += d * force;
+ }
+}
+
+ static void
+tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l)
+{
+ float hsl_c[3];
+ SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]);
+
+ if (do_h) {
+ hsl_c[0] += g_random_double_range(-0.5, 0.5) * force;
+ if (hsl_c[0] > 1) {
+ hsl_c[0] -= 1;
+ }
+ if (hsl_c[0] < 0) {
+ hsl_c[0] += 1;
+ }
+ }
+ if (do_s) {
+ hsl_c[1] += g_random_double_range(-hsl_c[1], 1 - hsl_c[1]) * force;
+ }
+ if (do_l) {
+ hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force;
+ }
+
+ SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]);
+}
+
+ static void
+tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l)
+{
+ if (mode == TWEAK_MODE_COLORPAINT) {
+ tweak_colorpaint (color, goal, force, do_h, do_s, do_l);
+ } else if (mode == TWEAK_MODE_COLORJITTER) {
+ tweak_colorjitter (color, force, do_h, do_s, do_l);
+ }
+}
+
+ static void
+tweak_opacity (guint mode, SPIScale24 *style_opacity, double opacity_goal, double force)
+{
+ double opacity = SP_SCALE24_TO_FLOAT (style_opacity->value);
+
+ if (mode == TWEAK_MODE_COLORPAINT) {
+ double d = opacity_goal - opacity;
+ opacity += d * force;
+ } else if (mode == TWEAK_MODE_COLORJITTER) {
+ opacity += g_random_double_range(-opacity, 1 - opacity) * force;
+ }
+
+ style_opacity->value = SP_SCALE24_FROM_FLOAT(opacity);
+}
+
+
+ static double
+tweak_profile (double dist, double radius)
+{
+ if (radius == 0) {
+ return 0;
+ }
+ double x = dist / radius;
+ double alpha = 1;
+ if (x >= 1) {
+ return 0;
+ } else if (x <= 0) {
+ return 1;
+ } else {
+ return (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5);
+ }
+}
+
+static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke,
+ guint32 const rgb_goal, Geom::Point p_w, double radius, double force, guint mode,
+ bool do_h, bool do_s, bool do_l, bool /*do_o*/)
+{
+ SPGradient *gradient = getGradient(item, fill_or_stroke);
+
+ if (!gradient || !dynamic_cast<SPGradient *>(gradient)) {
+ return;
+ }
+
+ Geom::Affine i2d (item->i2doc_affine ());
+ Geom::Point p = p_w * i2d.inverse();
+ p *= (gradient->gradientTransform).inverse();
+ // now p is in gradient's original coordinates
+
+ SPLinearGradient *lg = dynamic_cast<SPLinearGradient *>(gradient);
+ SPRadialGradient *rg = dynamic_cast<SPRadialGradient *>(gradient);
+ if (lg || rg) {
+
+ double pos = 0;
+ double r = 0;
+
+ if (lg) {
+ Geom::Point p1(lg->x1.computed, lg->y1.computed);
+ Geom::Point p2(lg->x2.computed, lg->y2.computed);
+ Geom::Point pdiff(p2 - p1);
+ double vl = Geom::L2(pdiff);
+
+ // This is the matrix which moves and rotates the gradient line
+ // so it's oriented along the X axis:
+ Geom::Affine norm = Geom::Affine(Geom::Translate(-p1)) *
+ Geom::Affine(Geom::Rotate(-atan2(pdiff[Geom::Y], pdiff[Geom::X])));
+
+ // Transform the mouse point by it to find out its projection onto the gradient line:
+ Geom::Point pnorm = p * norm;
+
+ // Scale its X coordinate to match the length of the gradient line:
+ pos = pnorm[Geom::X] / vl;
+ // Calculate radius in length-of-gradient-line units
+ r = radius / vl;
+
+ }
+ if (rg) {
+ Geom::Point c (rg->cx.computed, rg->cy.computed);
+ pos = Geom::L2(p - c) / rg->r.computed;
+ r = radius / rg->r.computed;
+ }
+
+ // Normalize pos to 0..1, taking into account gradient spread:
+ double pos_e = pos;
+ if (gradient->getSpread() == SP_GRADIENT_SPREAD_PAD) {
+ if (pos > 1) {
+ pos_e = 1;
+ }
+ if (pos < 0) {
+ pos_e = 0;
+ }
+ } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REPEAT) {
+ if (pos > 1 || pos < 0) {
+ pos_e = pos - floor(pos);
+ }
+ } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REFLECT) {
+ if (pos > 1 || pos < 0) {
+ bool odd = ((int)(floor(pos)) % 2 == 1);
+ pos_e = pos - floor(pos);
+ if (odd) {
+ pos_e = 1 - pos_e;
+ }
+ }
+ }
+
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary(gradient, false);
+
+ double offset_l = 0;
+ double offset_h = 0;
+ SPObject *child_prev = nullptr;
+ for (auto& child: vector->children) {
+ SPStop *stop = dynamic_cast<SPStop *>(&child);
+ if (!stop) {
+ continue;
+ }
+
+ offset_h = stop->offset;
+
+ if (child_prev) {
+ SPStop *prevStop = dynamic_cast<SPStop *>(child_prev);
+ g_assert(prevStop != nullptr);
+
+ if (offset_h - offset_l > r && pos_e >= offset_l && pos_e <= offset_h) {
+ // the summit falls in this interstop, and the radius is small,
+ // so it only affects the ends of this interstop;
+ // distribute the force between the two endstops so that they
+ // get all the painting even if they are not touched by the brush
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * (pos_e - offset_l) / (offset_h - offset_l),
+ do_h, do_s, do_l);
+ tweak_color(mode, prevStop->getColor().v.c, rgb_goal,
+ force * (offset_h - pos_e) / (offset_h - offset_l),
+ do_h, do_s, do_l);
+ stop->updateRepr();
+ child_prev->updateRepr();
+ break;
+ } else {
+ // wide brush, may affect more than 2 stops,
+ // paint each stop by the force from the profile curve
+ if (offset_l <= pos_e && offset_l > pos_e - r) {
+ tweak_color(mode, prevStop->getColor().v.c, rgb_goal,
+ force * tweak_profile (fabs (pos_e - offset_l), r),
+ do_h, do_s, do_l);
+ child_prev->updateRepr();
+ }
+
+ if (offset_h >= pos_e && offset_h < pos_e + r) {
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * tweak_profile (fabs (pos_e - offset_h), r),
+ do_h, do_s, do_l);
+ stop->updateRepr();
+ }
+ }
+ }
+
+ offset_l = offset_h;
+ child_prev = &child;
+ }
+ } else {
+ // Mesh
+ SPMeshGradient *mg = dynamic_cast<SPMeshGradient *>(gradient);
+ if (mg) {
+ SPMeshGradient *mg_array = dynamic_cast<SPMeshGradient *>(mg->getArray());
+ SPMeshNodeArray *array = &(mg_array->array);
+ // Every third node is a corner node
+ for( unsigned i=0; i < array->nodes.size(); i+=3 ) {
+ for( unsigned j=0; j < array->nodes[i].size(); j+=3 ) {
+ SPStop *stop = array->nodes[i][j]->stop;
+ double distance = Geom::L2(Geom::Point(p - array->nodes[i][j]->p));
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * tweak_profile (distance, radius), do_h, do_s, do_l);
+ stop->updateRepr();
+ }
+ }
+ }
+ }
+}
+
+ static bool
+sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point,
+ guint32 fill_goal, bool do_fill,
+ guint32 stroke_goal, bool do_stroke,
+ float opacity_goal, bool do_opacity,
+ bool do_blur, bool reverse,
+ Geom::Point p, double radius, double force,
+ bool do_h, bool do_s, bool do_l, bool do_o)
+{
+ bool did = false;
+
+ if (dynamic_cast<SPGroup *>(item)) {
+ for (auto& child: item->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&child);
+ if (childItem) {
+ if (sp_tweak_color_recursive (mode, childItem, item_at_point,
+ fill_goal, do_fill,
+ stroke_goal, do_stroke,
+ opacity_goal, do_opacity,
+ do_blur, reverse,
+ p, radius, force, do_h, do_s, do_l, do_o)) {
+ did = true;
+ }
+ }
+ }
+
+ } else {
+ SPStyle *style = item->style;
+ if (!style) {
+ return false;
+ }
+ Geom::OptRect bbox = item->documentGeometricBounds();
+ if (!bbox) {
+ return false;
+ }
+
+ Geom::Rect brush(p - Geom::Point(radius, radius), p + Geom::Point(radius, radius));
+
+ Geom::Point center = bbox->midpoint();
+ double this_force;
+
+ // if item == item_at_point, use max force
+ if (item == item_at_point) {
+ this_force = force;
+ // else if no overlap of bbox and brush box, skip:
+ } else if (!bbox->intersects(brush)) {
+ return false;
+ //TODO:
+ // else if object > 1.5 brush: test 4/8/16 points in the brush on hitting the object, choose max
+ //} else if (bbox->maxExtent() > 3 * radius) {
+ //}
+ // else if object > 0.5 brush: test 4 corners of bbox and center on being in the brush, choose max
+ // else if still smaller, then check only the object center:
+ } else {
+ this_force = force * tweak_profile (Geom::L2 (p - center), radius);
+ }
+
+ if (this_force > 0.002) {
+
+ if (do_blur) {
+ Geom::OptRect bbox = item->documentGeometricBounds();
+ if (!bbox) {
+ return did;
+ }
+
+ double blur_now = 0;
+ Geom::Affine i2dt = item->i2dt_affine ();
+ if (style->filter.set && style->getFilter()) {
+ //cycle through filter primitives
+ for (auto& primitive_obj: style->getFilter()->children) {
+ SPFilterPrimitive *primitive = dynamic_cast<SPFilterPrimitive *>(&primitive_obj);
+ if (primitive) {
+ //if primitive is gaussianblur
+ SPGaussianBlur * spblur = dynamic_cast<SPGaussianBlur *>(primitive);
+ if (spblur) {
+ float num = spblur->stdDeviation.getNumber();
+ blur_now += num * i2dt.descrim(); // sum all blurs in the filter
+ }
+ }
+ }
+ }
+ double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y];
+ blur_now = blur_now / perimeter;
+
+ double blur_new;
+ if (reverse) {
+ blur_new = blur_now - 0.06 * force;
+ } else {
+ blur_new = blur_now + 0.06 * force;
+ }
+ if (blur_new < 0.0005 && blur_new < blur_now) {
+ blur_new = 0;
+ }
+ if (blur_new == 0) {
+ remove_filter(item, false);
+ } else {
+ double radius = blur_new * perimeter;
+ SPFilter *filter = modify_filter_gaussian_blur_from_item(item->document, item, radius);
+ sp_style_set_property_url(item, "filter", filter, false);
+ }
+ return true; // do not do colors, blur is a separate mode
+ }
+
+ if (do_fill) {
+ if (style->fill.isPaintserver()) {
+ tweak_colors_in_gradient(item, Inkscape::FOR_FILL, fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o);
+ did = true;
+ } else if (style->fill.isColor()) {
+ tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l);
+ item->updateRepr();
+ did = true;
+ }
+ }
+ if (do_stroke) {
+ if (style->stroke.isPaintserver()) {
+ tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o);
+ did = true;
+ } else if (style->stroke.isColor()) {
+ tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l);
+ item->updateRepr();
+ did = true;
+ }
+ }
+ if (do_opacity && do_o) {
+ tweak_opacity (mode, &style->opacity, opacity_goal, this_force);
+ }
+ }
+}
+
+return did;
+}
+
+
+ static bool
+sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point vector, bool reverse)
+{
+ Inkscape::Selection *selection = tc->desktop->getSelection();
+ SPDesktop *desktop = SP_EVENT_CONTEXT(tc)->desktop;
+
+ if (selection->isEmpty()) {
+ return false;
+ }
+
+ bool did = false;
+ double radius = get_dilate_radius(tc);
+
+ SPItem *item_at_point = SP_EVENT_CONTEXT(tc)->desktop->getItemAtPoint(event_p, TRUE);
+
+ bool do_fill = false, do_stroke = false, do_opacity = false;
+ guint32 fill_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", true, &do_fill);
+ guint32 stroke_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", false, &do_stroke);
+ double opacity_goal = sp_desktop_get_master_opacity_tool(desktop, "/tools/tweak", &do_opacity);
+ if (reverse) {
+#if 0
+ // HSL inversion
+ float hsv[3];
+ float rgb[3];
+ SPColor::rgb_to_hsv_floatv (hsv,
+ SP_RGBA32_R_F(fill_goal),
+ SP_RGBA32_G_F(fill_goal),
+ SP_RGBA32_B_F(fill_goal));
+ SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]);
+ fill_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1);
+ SPColor::rgb_to_hsv_floatv (hsv,
+ SP_RGBA32_R_F(stroke_goal),
+ SP_RGBA32_G_F(stroke_goal),
+ SP_RGBA32_B_F(stroke_goal));
+ SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]);
+ stroke_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1);
+#else
+ // RGB inversion
+ fill_goal = SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(fill_goal)),
+ (255 - SP_RGBA32_G_U(fill_goal)),
+ (255 - SP_RGBA32_B_U(fill_goal)),
+ (255 - SP_RGBA32_A_U(fill_goal)));
+ stroke_goal = SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(stroke_goal)),
+ (255 - SP_RGBA32_G_U(stroke_goal)),
+ (255 - SP_RGBA32_B_U(stroke_goal)),
+ (255 - SP_RGBA32_A_U(stroke_goal)));
+#endif
+ opacity_goal = 1 - opacity_goal;
+ }
+
+ double path_force = get_path_force(tc);
+ if (radius == 0 || path_force == 0) {
+ return false;
+ }
+ double move_force = get_move_force(tc);
+ double color_force = MIN(sqrt(path_force)/20.0, 1);
+
+ // auto items= selection->items();
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+ for(auto item : items){
+ if (is_color_mode (tc->mode)) {
+ if (do_fill || do_stroke || do_opacity) {
+ if (sp_tweak_color_recursive (tc->mode, item, item_at_point,
+ fill_goal, do_fill,
+ stroke_goal, do_stroke,
+ opacity_goal, do_opacity,
+ tc->mode == TWEAK_MODE_BLUR, reverse,
+ p, radius, color_force, tc->do_h, tc->do_s, tc->do_l, tc->do_o)) {
+ did = true;
+ }
+ }
+ } else if (is_transform_mode(tc->mode)) {
+ if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, move_force, tc->fidelity, reverse)) {
+ did = true;
+ }
+ } else {
+ if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, path_force, tc->fidelity, reverse)) {
+ did = true;
+ }
+ }
+ }
+
+ return did;
+}
+
+ static void
+sp_tweak_update_area (TweakTool *tc)
+{
+ double radius = get_dilate_radius(tc);
+ Geom::Affine const sm (Geom::Scale(radius, radius) * Geom::Translate(SP_EVENT_CONTEXT(tc)->desktop->point()));
+ sp_canvas_item_affine_absolute(tc->dilate_area, sm);
+ sp_canvas_item_show(tc->dilate_area);
+}
+
+ static void
+sp_tweak_switch_mode (TweakTool *tc, gint mode, bool with_shift)
+{
+ auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("TweakToolbar"));
+
+ if(tb) {
+ tb->set_mode(mode);
+ } else {
+ std::cerr << "Could not access Tweak toolbar" << std::endl;
+ }
+
+ // need to set explicitly, because the prefs may not have changed by the previous
+ tc->mode = mode;
+ tc->update_cursor(with_shift);
+}
+
+ static void
+sp_tweak_switch_mode_temporarily (TweakTool *tc, gint mode, bool with_shift)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // Juggling about so that prefs have the old value but tc->mode and the button show new mode:
+ gint now_mode = prefs->getInt("/tools/tweak/mode", 0);
+
+ auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("TweakToolbar"));
+
+ if(tb) {
+ tb->set_mode(mode);
+ } else {
+ std::cerr << "Could not access Tweak toolbar" << std::endl;
+ }
+
+ // button has changed prefs, restore
+ prefs->setInt("/tools/tweak/mode", now_mode);
+ // changing prefs changed tc->mode, restore back :
+ tc->mode = mode;
+ tc->update_cursor(with_shift);
+}
+
+bool TweakTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ sp_canvas_item_show(this->dilate_area);
+ break;
+ case GDK_LEAVE_NOTIFY:
+ sp_canvas_item_hide(this->dilate_area);
+ break;
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1 && !this->space_panning) {
+
+ if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+
+ Geom::Point const button_w(event->button.x,
+ event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+ this->last_push = desktop->dt2doc(button_dt);
+
+ sp_tweak_extinput(this, event);
+
+ desktop->canvas->forceFullRedrawAfterInterruptions(3);
+ this->is_drawing = true;
+ this->is_dilating = true;
+ this->has_dilated = false;
+
+ ret = TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ {
+ Geom::Point const motion_w(event->motion.x,
+ event->motion.y);
+ Geom::Point motion_dt(desktop->w2d(motion_w));
+ Geom::Point motion_doc(desktop->dt2doc(motion_dt));
+ sp_tweak_extinput(this, event);
+
+ // draw the dilating cursor
+ double radius = get_dilate_radius(this);
+ Geom::Affine const sm (Geom::Scale(radius, radius) * Geom::Translate(desktop->w2d(motion_w)));
+ sp_canvas_item_affine_absolute(this->dilate_area, sm);
+ sp_canvas_item_show(this->dilate_area);
+
+ guint num = 0;
+ if (!desktop->selection->isEmpty()) {
+ num = (guint) boost::distance(desktop->selection->items());
+ }
+ if (num == 0) {
+ this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to tweak."));
+ }
+
+ // dilating:
+ if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) {
+ sp_tweak_dilate (this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false);
+ //this->last_push = motion_doc;
+ this->has_dilated = true;
+ // it's slow, so prevent clogging up with events
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ return TRUE;
+ }
+
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ {
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+
+ desktop->canvas->endForcedFullRedraws();
+ this->is_drawing = false;
+
+ if (this->is_dilating && event->button.button == 1 && !this->space_panning) {
+ if (!this->has_dilated) {
+ // if we did not rub, do a light tap
+ this->pressure = 0.03;
+ sp_tweak_dilate (this, motion_w, desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event));
+ }
+ this->is_dilating = false;
+ this->has_dilated = false;
+ switch (this->mode) {
+ case TWEAK_MODE_MOVE:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Move tweak"));
+ break;
+ case TWEAK_MODE_MOVE_IN_OUT:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Move in/out tweak"));
+ break;
+ case TWEAK_MODE_MOVE_JITTER:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Move jitter tweak"));
+ break;
+ case TWEAK_MODE_SCALE:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Scale tweak"));
+ break;
+ case TWEAK_MODE_ROTATE:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Rotate tweak"));
+ break;
+ case TWEAK_MODE_MORELESS:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Duplicate/delete tweak"));
+ break;
+ case TWEAK_MODE_PUSH:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Push path tweak"));
+ break;
+ case TWEAK_MODE_SHRINK_GROW:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Shrink/grow path tweak"));
+ break;
+ case TWEAK_MODE_ATTRACT_REPEL:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Attract/repel path tweak"));
+ break;
+ case TWEAK_MODE_ROUGHEN:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Roughen path tweak"));
+ break;
+ case TWEAK_MODE_COLORPAINT:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Color paint tweak"));
+ break;
+ case TWEAK_MODE_COLORJITTER:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Color jitter tweak"));
+ break;
+ case TWEAK_MODE_BLUR:
+ DocumentUndo::done(this->desktop->getDocument(),
+ SP_VERB_CONTEXT_TWEAK, _("Blur tweak"));
+ break;
+ }
+ }
+ break;
+ }
+ case GDK_KEY_PRESS:
+ {
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_m:
+ case GDK_KEY_M:
+ case GDK_KEY_0:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_MOVE, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_i:
+ case GDK_KEY_I:
+ case GDK_KEY_1:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_IN_OUT, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ case GDK_KEY_2:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_JITTER, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_less:
+ case GDK_KEY_comma:
+ case GDK_KEY_greater:
+ case GDK_KEY_period:
+ case GDK_KEY_3:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_SCALE, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_bracketright:
+ case GDK_KEY_bracketleft:
+ case GDK_KEY_4:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_ROTATE, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_d:
+ case GDK_KEY_D:
+ case GDK_KEY_5:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_MORELESS, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_p:
+ case GDK_KEY_P:
+ case GDK_KEY_6:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_PUSH, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+ case GDK_KEY_7:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ case GDK_KEY_8:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_ATTRACT_REPEL, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_r:
+ case GDK_KEY_R:
+ case GDK_KEY_9:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_ROUGHEN, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_COLORPAINT, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_j:
+ case GDK_KEY_J:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_COLORJITTER, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_b:
+ case GDK_KEY_B:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_tweak_switch_mode(this, TWEAK_MODE_BLUR, MOD__SHIFT(event));
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->force += 0.05;
+ if (this->force > 1.0) {
+ this->force = 1.0;
+ }
+ desktop->setToolboxAdjustmentValue ("tweak-force", this->force * 100);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->force -= 0.05;
+ if (this->force < 0.0) {
+ this->force = 0.0;
+ }
+ desktop->setToolboxAdjustmentValue ("tweak-force", this->force * 100);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width += 0.01;
+ if (this->width > 1.0) {
+ this->width = 1.0;
+ }
+ desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); // the same spinbutton is for alt+x
+ sp_tweak_update_area(this);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL_ONLY(event)) {
+ this->width -= 0.01;
+ if (this->width < 0.01) {
+ this->width = 0.01;
+ }
+ desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100);
+ sp_tweak_update_area(this);
+ ret = TRUE;
+ }
+ break;
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ this->width = 0.01;
+ desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100);
+ sp_tweak_update_area(this);
+ ret = TRUE;
+ break;
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ this->width = 1.0;
+ desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100);
+ sp_tweak_update_area(this);
+ ret = TRUE;
+ break;
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ desktop->setToolboxFocusTo ("tweak-width");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->update_cursor(true);
+ break;
+
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ sp_tweak_switch_mode_temporarily(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event));
+ break;
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ case GDK_KEY_RELEASE: {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->update_cursor(false);
+ break;
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event));
+ this->message_context->clear();
+ break;
+ default:
+ sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event));
+ break;
+ }
+ }
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+}
+}
+}
+
+/*
+ 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/tools/tweak-tool.h b/src/ui/tools/tweak-tool.h
new file mode 100644
index 0000000..677cc0b
--- /dev/null
+++ b/src/ui/tools/tweak-tool.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_TWEAK_CONTEXT_H__
+#define __SP_TWEAK_CONTEXT_H__
+
+/*
+ * tweaking paths without node editing
+ *
+ * Authors:
+ * bulia byak
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+#include <2geom/point.h>
+
+#define SAMPLING_SIZE 8 /* fixme: ?? */
+
+#define TC_MIN_PRESSURE 0.0
+#define TC_MAX_PRESSURE 1.0
+#define TC_DEFAULT_PRESSURE 0.35
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum {
+ TWEAK_MODE_MOVE,
+ TWEAK_MODE_MOVE_IN_OUT,
+ TWEAK_MODE_MOVE_JITTER,
+ TWEAK_MODE_SCALE,
+ TWEAK_MODE_ROTATE,
+ TWEAK_MODE_MORELESS,
+ TWEAK_MODE_PUSH,
+ TWEAK_MODE_SHRINK_GROW,
+ TWEAK_MODE_ATTRACT_REPEL,
+ TWEAK_MODE_ROUGHEN,
+ TWEAK_MODE_COLORPAINT,
+ TWEAK_MODE_COLORJITTER,
+ TWEAK_MODE_BLUR
+};
+
+class TweakTool : public ToolBase {
+public:
+ TweakTool();
+ ~TweakTool() override;
+
+ /* extended input data */
+ gdouble pressure;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressure;
+ bool usetilt;
+
+ double width;
+ double force;
+ double fidelity;
+
+ gint mode;
+
+ bool is_drawing;
+
+ bool is_dilating;
+ bool has_dilated;
+ Geom::Point last_push;
+ SPCanvasItem *dilate_area;
+
+ bool do_h;
+ bool do_s;
+ bool do_l;
+ bool do_o;
+
+ sigc::connection style_set_connection;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+ void update_cursor(bool with_shift);
+
+private:
+ bool set_style(const SPCSSAttr* css);
+};
+
+}
+}
+}
+
+#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/tools/zoom-tool.cpp b/src/ui/tools/zoom-tool.cpp
new file mode 100644
index 0000000..0372e8d
--- /dev/null
+++ b/src/ui/tools/zoom-tool.cpp
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Handy zooming tool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 1999-2002 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <gdk/gdkkeysyms.h>
+
+#include "include/macros.h"
+#include "rubberband.h"
+#include "display/sp-canvas-item.h"
+#include "display/sp-canvas-util.h"
+#include "desktop.h"
+#include "ui/pixmaps/cursor-zoom.xpm"
+#include "ui/pixmaps/cursor-zoom-out.xpm"
+#include "selection-chemistry.h"
+
+#include "ui/tools/zoom-tool.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+const std::string& ZoomTool::getPrefsPath() {
+ return ZoomTool::prefsPath;
+}
+
+const std::string ZoomTool::prefsPath = "/tools/zoom";
+
+ZoomTool::ZoomTool()
+ : ToolBase(cursor_zoom_xpm)
+ , grabbed(nullptr)
+ , escaped(false)
+{
+}
+
+ZoomTool::~ZoomTool() = default;
+
+void ZoomTool::finish() {
+ this->enableGrDrag(false);
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ ToolBase::finish();
+}
+
+void ZoomTool::setup() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/zoom/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/zoom/gradientdrag")) {
+ this->enableGrDrag();
+ }
+
+ ToolBase::setup();
+}
+
+bool ZoomTool::root_handler(GdkEvent* event) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ double const zoom_inc = prefs->getDoubleLimited("/options/zoomincrement/value", M_SQRT2, 1.01, 10);
+
+ bool ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ {
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+
+ if (event->button.button == 1 && !this->space_panning) {
+ // save drag origin
+ xp = (gint) event->button.x;
+ yp = (gint) event->button.y;
+ within_tolerance = true;
+
+ Inkscape::Rubberband::get(desktop)->start(desktop, button_dt);
+
+ escaped = false;
+
+ ret = true;
+ } else if (event->button.button == 3) {
+ double const zoom_rel( (event->button.state & GDK_SHIFT_MASK)
+ ? zoom_inc
+ : 1 / zoom_inc );
+
+ desktop->zoom_relative_keep_point(button_dt, zoom_rel);
+ ret = true;
+ }
+
+ sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK,
+ nullptr, event->button.time);
+
+ this->grabbed = SP_CANVAS_ITEM(desktop->acetate);
+ break;
+ }
+
+ case GDK_MOTION_NOTIFY:
+ if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) {
+ ret = true;
+
+ if ( within_tolerance
+ && ( abs( (gint) event->motion.x - xp ) < tolerance )
+ && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) {
+ break; // do not drag if we're within tolerance from origin
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(desktop->w2d(motion_w));
+ Inkscape::Rubberband::get(desktop)->move(motion_dt);
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ {
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+
+ if ( event->button.button == 1 && !this->space_panning) {
+ Geom::OptRect const b = Inkscape::Rubberband::get(desktop)->getRectangle();
+
+ if (b && !within_tolerance && !(GDK_SHIFT_MASK & event->button.state) ) {
+ desktop->set_display_area(*b, 10);
+ } else if (!escaped) {
+ double const zoom_rel( (event->button.state & GDK_SHIFT_MASK)
+ ? 1 / zoom_inc
+ : zoom_inc );
+
+ desktop->zoom_relative_keep_point(button_dt, zoom_rel);
+ }
+
+ ret = true;
+ }
+
+ Inkscape::Rubberband::get(desktop)->stop();
+
+ if (this->grabbed) {
+ sp_canvas_item_ungrab(this->grabbed);
+ this->grabbed = nullptr;
+ }
+
+ xp = yp = 0;
+ escaped = false;
+ break;
+ }
+ case GDK_KEY_PRESS:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Escape:
+ if (!Inkscape::Rubberband::get(desktop)->is_started()) {
+ Inkscape::SelectionHelper::selectNone(desktop);
+ }
+
+ Inkscape::Rubberband::get(desktop)->stop();
+ xp = yp = 0;
+ escaped = true;
+ ret = true;
+ break;
+
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_Down:
+ // prevent the zoom field from activation
+ if (!MOD__CTRL_ONLY(event))
+ ret = true;
+ break;
+
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->cursor_shape = cursor_zoom_out_xpm;
+ this->sp_event_context_update_cursor();
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ default:
+ break;
+ }
+ break;
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ this->cursor_shape = cursor_zoom_xpm;
+ this->sp_event_context_update_cursor();
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+}
+}
+}
+
+/*
+ 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/tools/zoom-tool.h b/src/ui/tools/zoom-tool.h
new file mode 100644
index 0000000..003b6be
--- /dev/null
+++ b/src/ui/tools/zoom-tool.h
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_ZOOM_CONTEXT_H__
+#define __SP_ZOOM_CONTEXT_H__
+
+/*
+ * Handy zooming tool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ *
+ * Copyright (C) 1999-2002 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+
+#define SP_ZOOM_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ZoomTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_ZOOM_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ZoomTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ZoomTool : public ToolBase {
+public:
+ ZoomTool();
+ ~ZoomTool() override;
+
+ static const std::string prefsPath;
+
+ void setup() override;
+ void finish() override;
+ bool root_handler(GdkEvent* event) override;
+
+ const std::string& getPrefsPath() override;
+
+private:
+ SPCanvasItem *grabbed;
+ bool escaped;
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/util.cpp b/src/ui/util.cpp
new file mode 100644
index 0000000..1832cf7
--- /dev/null
+++ b/src/ui/util.cpp
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Utility functions for UI
+ *
+ * Authors:
+ * Tavmjong Bah
+ * John Smith
+ *
+ * Copyright (C) 2004, 2013, 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "util.h"
+
+/*
+ * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text"
+ * Text should be > length 8 or just return the original text
+ */
+Glib::ustring ink_ellipsize_text(Glib::ustring const &src, size_t maxlen)
+{
+ if (src.length() > maxlen && maxlen > 8) {
+ size_t p1 = (size_t) maxlen / 2;
+ size_t p2 = (size_t) src.length() - (maxlen - p1 - 1);
+ return src.substr(0, p1) + "
" + src.substr(p2);
+ }
+ return src;
+}
+
+/*
+ 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/util.h b/src/ui/util.h
new file mode 100644
index 0000000..9480371
--- /dev/null
+++ b/src/ui/util.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Utility functions for UI
+ *
+ * Authors:
+ * Tavmjong Bah
+ * John Smith
+ *
+ * Copyright (C) 2013, 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef UI_UTIL_SEEN
+#define UI_UTIL_SEEN
+
+#include <glibmm/ustring.h>
+
+Glib::ustring ink_ellipsize_text (Glib::ustring const &src, size_t maxlen);
+
+#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/uxmanager.cpp b/src/ui/uxmanager.cpp
new file mode 100644
index 0000000..1f62ece
--- /dev/null
+++ b/src/ui/uxmanager.cpp
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Desktop widget implementation.
+ */
+/* Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <vector>
+#include "widgets/desktop-widget.h"
+
+#include "uxmanager.h"
+#include "desktop.h"
+#include "ui/monitor.h"
+#include "util/ege-tags.h"
+#include "widgets/toolbox.h"
+
+class TrackItem
+{
+public:
+ TrackItem() :
+ destroyConn(),
+ boxes()
+ {}
+
+ sigc::connection destroyConn;
+ std::vector<GtkWidget*> boxes;
+};
+
+static std::vector<SPDesktop*> desktops;
+static std::vector<SPDesktopWidget*> dtws;
+static std::map<SPDesktop*, TrackItem> trackedBoxes;
+
+
+namespace {
+
+void desktopDestructHandler(SPDesktop *desktop)
+{
+ std::map<SPDesktop*, TrackItem>::iterator it = trackedBoxes.find(desktop);
+ if (it != trackedBoxes.end())
+ {
+ trackedBoxes.erase(it);
+ }
+}
+
+
+// TODO unify this later:
+static Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view )
+{
+ Glib::ustring prefPath;
+
+ if (reinterpret_cast<SPDesktop*>(view)->is_focusMode()) {
+ prefPath = "/focus/";
+ } else if (reinterpret_cast<SPDesktop*>(view)->is_fullscreen()) {
+ prefPath = "/fullscreen/";
+ } else {
+ prefPath = "/window/";
+ }
+
+ return prefPath;
+}
+
+}
+
+namespace Inkscape {
+namespace UI {
+
+UXManager* instance = nullptr;
+
+class UXManagerImpl : public UXManager
+{
+public:
+ UXManagerImpl();
+ ~UXManagerImpl() override;
+
+ void addTrack( SPDesktopWidget* dtw ) override;
+ void delTrack( SPDesktopWidget* dtw ) override;
+
+ void connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) override;
+
+ gint getDefaultTask( SPDesktop *desktop ) override;
+ void setTask(SPDesktop* dt, gint val) override;
+
+ bool isWidescreen() const override;
+
+private:
+ bool _widescreen;
+};
+
+UXManager* UXManager::getInstance()
+{
+ if (!instance) {
+ instance = new UXManagerImpl();
+ }
+ return instance;
+}
+
+
+UXManager::UXManager()
+= default;
+
+UXManager::~UXManager()
+= default;
+
+UXManagerImpl::UXManagerImpl() :
+ _widescreen(false)
+{
+ ege::TagSet tags;
+ tags.setLang("en");
+
+ tags.addTag(ege::Tag("General"));
+ tags.addTag(ege::Tag("Icons"));
+
+ // Figure out if we're on a widescreen display
+ Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary();
+ int const width = monitor_geometry.get_width();
+ int const height = monitor_geometry.get_height();
+
+ if (width && height) {
+ gdouble aspect = static_cast<gdouble>(width) / static_cast<gdouble>(height);
+ if (aspect > 1.65) {
+ _widescreen = true;
+ }
+ }
+}
+
+UXManagerImpl::~UXManagerImpl()
+= default;
+
+bool UXManagerImpl::isWidescreen() const
+{
+ return _widescreen;
+}
+
+gint UXManagerImpl::getDefaultTask( SPDesktop *desktop )
+{
+ gint taskNum = isWidescreen() ? 2 : 0;
+
+ Glib::ustring prefPath = getLayoutPrefPath( desktop );
+ taskNum = Inkscape::Preferences::get()->getInt( prefPath + "task/taskset", taskNum );
+ taskNum = (taskNum < 0) ? 0 : (taskNum > 2) ? 2 : taskNum;
+
+ return taskNum;
+}
+
+void UXManagerImpl::setTask(SPDesktop* dt, gint val)
+{
+ for (auto dtw : dtws) {
+ gboolean notDone = Inkscape::Preferences::get()->getBool("/options/workarounds/dynamicnotdone", false);
+
+ if (dtw->desktop == dt) {
+ int taskNum = val;
+ switch (val) {
+ default:
+ case 0:
+ dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT);
+ dtw->setToolboxPosition("CommandsToolbar", GTK_POS_TOP);
+ if (notDone) {
+ dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP);
+ }
+ dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT);
+ taskNum = val; // in case it was out of range
+ break;
+ case 1:
+ dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT);
+ dtw->setToolboxPosition("CommandsToolbar", GTK_POS_TOP);
+ if (notDone) {
+ dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP);
+ }
+ dtw->setToolboxPosition("SnapToolbar", GTK_POS_TOP);
+ break;
+ case 2:
+ dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT);
+ dtw->setToolboxPosition("CommandsToolbar", GTK_POS_RIGHT);
+ dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT);
+ if (notDone) {
+ dtw->setToolboxPosition("AuxToolbar", GTK_POS_RIGHT);
+ }
+ }
+ Glib::ustring prefPath = getLayoutPrefPath( dtw->desktop );
+ Inkscape::Preferences::get()->setInt( prefPath + "task/taskset", taskNum );
+ }
+ }
+}
+
+
+void UXManagerImpl::addTrack( SPDesktopWidget* dtw )
+{
+ if (std::find(dtws.begin(), dtws.end(), dtw) == dtws.end()) {
+ dtws.push_back(dtw);
+ }
+}
+
+void UXManagerImpl::delTrack( SPDesktopWidget* dtw )
+{
+ std::vector<SPDesktopWidget*>::iterator iter = std::find(dtws.begin(), dtws.end(), dtw);
+ if (iter != dtws.end()) {
+ dtws.erase(iter);
+ }
+}
+
+void UXManagerImpl::connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop )
+{
+ if (!desktop)
+ {
+ return;
+ }
+ TrackItem &tracker = trackedBoxes[desktop];
+ std::vector<GtkWidget*>& tracked = tracker.boxes;
+ tracker.destroyConn = desktop->connectDestroy(&desktopDestructHandler);
+
+ for (auto toolbox : toolboxes) {
+ ToolboxFactory::setToolboxDesktop( toolbox, desktop );
+ if (find(tracked.begin(), tracked.end(), toolbox) == tracked.end()) {
+ tracked.push_back(toolbox);
+ }
+ }
+
+ if (std::find(desktops.begin(), desktops.end(), desktop) == desktops.end()) {
+ desktops.push_back(desktop);
+ }
+
+ gint taskNum = getDefaultTask( desktop );
+
+ // note: this will change once more options are in the task set support:
+ Inkscape::UI::UXManager::getInstance()->setTask( desktop, taskNum );
+}
+
+
+} // 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 :
diff --git a/src/ui/uxmanager.h b/src/ui/uxmanager.h
new file mode 100644
index 0000000..5cb799c
--- /dev/null
+++ b/src/ui/uxmanager.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_UXMANAGER_H
+#define SEEN_UI_UXMANAGER_H
+/*
+ * A simple interface for previewing representations.
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <vector>
+#include <glib.h>
+
+extern "C" typedef struct _GtkWidget GtkWidget;
+class SPDesktop;
+struct SPDesktopWidget;
+
+namespace Inkscape {
+namespace UI {
+
+class UXManager
+{
+public:
+ static UXManager* getInstance();
+ virtual ~UXManager();
+
+ virtual void addTrack( SPDesktopWidget* dtw ) = 0;
+ virtual void delTrack( SPDesktopWidget* dtw ) = 0;
+
+ virtual void connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) = 0;
+
+ virtual gint getDefaultTask( SPDesktop *desktop ) = 0;
+ virtual void setTask( SPDesktop* dt, gint val ) = 0;
+
+ virtual bool isWidescreen() const = 0;
+
+ UXManager( UXManager const & ) = delete;
+ UXManager & operator=( UXManager const & ) = delete;
+
+protected:
+ UXManager();
+};
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_UXMANAGER_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/view/README b/src/ui/view/README
new file mode 100644
index 0000000..9316f8b
--- /dev/null
+++ b/src/ui/view/README
@@ -0,0 +1,51 @@
+
+This directory contains the class Inkscape::UI::View::View and related items.
+
+View is an abstract base class for all UI document views. Documents
+can be displayed by more than one window, each having its own view
+(e.g. zoom level, selection, etc.).
+
+View is the base class for:
+
+* SPDesktop
+* SVGView REMOVED
+
+SPViewWidget is the base for:
+
+* SPDocumentWidget
+* SPSVGViewWidget REMOVED
+
+SPSVGViewWidget has been replaced by SVGViewWidget, see below.
+
+
+SPViewWidget:
+ Contains a GtkEventBox and holds a View.
+
+SPDesktopWidget:
+ Contains:
+ VBox
+ HBox
+ GtkGrid
+ GtkPaned
+ GtkGrid
+ SPCanvas
+ Plus lots of other junk.
+
+
+SVGViewWidget:
+ Used many places as a convenient way to show an SVG (file dialog, Inkview).
+ Derived, rather uselessly, from Gtk::Scrollbar.
+ It no longer is dependent on View (and really doesn't belong here anymore).
+
+ It contains: SPCanvas
+
+To do:
+
+
+* Convert everything to C++.
+* Evaluate moving SPDesktopWidget down the widget stack.
+ It doesn't use the EventBox of SPViewWidget!
+
+A DesktopViewWidget should contain:
+ DesktopView (aka SPDesktop)
+ SPCanvas
diff --git a/src/ui/view/edit-widget-interface.h b/src/ui/view/edit-widget-interface.h
new file mode 100644
index 0000000..eb3e33b
--- /dev/null
+++ b/src/ui/view/edit-widget-interface.h
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * John Bintz <jcoswell@coswellproductions.org>
+ *
+ * Copyright (C) 2006 John Bintz
+ * Copyright (C) 2005 Ralf Stephan
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_H
+#define INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_H
+
+#include "message.h"
+#include <2geom/point.h>
+
+namespace Gtk {
+ class Toolbar;
+ class Window;
+}
+
+namespace Glib {
+class ustring;
+}
+
+namespace Inkscape { namespace UI { namespace Widget { class Dock; } } }
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+/**
+ * Abstract base class for all EditWidget implementations.
+ */
+struct EditWidgetInterface
+{
+ EditWidgetInterface() = default;
+ virtual ~EditWidgetInterface() = default;
+
+ /// Returns pointer to window UI object as void*
+ virtual Gtk::Window *getWindow() = 0;
+
+ /// Set the widget's title
+ virtual void setTitle (gchar const*) = 0;
+
+ /// Show all parts of widget the user wants to see.
+ virtual void layout() = 0;
+
+ /// Present widget to user
+ virtual void present() = 0;
+
+ /// Returns geometry of widget
+ virtual void getGeometry (gint &x, gint &y, gint &w, gint &h) = 0;
+
+ /// Change the widget's size
+ virtual void setSize (gint w, gint h) = 0;
+
+ /// Move widget to specified position
+ virtual void setPosition (Geom::Point p) = 0;
+
+ /// Transientize widget
+ virtual void setTransient (void*, int) = 0;
+
+ /// Return mouse position in widget
+ virtual Geom::Point getPointer() = 0;
+
+ /// Make widget iconified
+ virtual void setIconified() = 0;
+
+ /// Make widget maximized on screen
+ virtual void setMaximized() = 0;
+
+ /// Make widget fill screen and show it if possible.
+ virtual void setFullscreen() = 0;
+
+ /// Shuts down the desktop object for the view being closed. It checks
+ /// to see if the document has been edited, and if so prompts the user
+ /// to save, discard, or cancel. Returns TRUE if the shutdown operation
+ /// is cancelled or if the save is cancelled or fails, FALSE otherwise.
+ virtual bool shutdown() = 0;
+
+ /// Destroy and delete widget.
+ virtual void destroy() = 0;
+
+
+ /// Store window position to prefs
+ virtual void storeDesktopPosition() = 0;
+
+ /// Queue a redraw request with the canvas
+ virtual void requestCanvasUpdate() = 0;
+
+ /// Force a redraw of the canvas
+ virtual void requestCanvasUpdateAndWait() = 0;
+
+ /// Enable interaction on this desktop
+ virtual void enableInteraction() = 0;
+
+ /// Disable interaction on this desktop
+ virtual void disableInteraction() = 0;
+
+ /// Update the "active desktop" indicator
+ virtual void activateDesktop() = 0;
+
+ /// Update the "inactive desktop" indicator
+ virtual void deactivateDesktop() = 0;
+
+ /// Update rulers from current values
+ virtual void updateRulers() = 0;
+
+ /// Update scrollbars from current values
+ virtual void updateScrollbars (double scale) = 0;
+
+ /// Toggle rulers on/off and set preference value accordingly
+ virtual void toggleRulers() = 0;
+
+ /// Toggle scrollbars on/off and set preference value accordingly
+ virtual void toggleScrollbars() = 0;
+
+ /// Toggle CMS on/off and set preference value accordingly
+ virtual void toggleColorProfAdjust() = 0;
+
+ /// Is CMS on/off
+ virtual bool colorProfAdjustEnabled() = 0;
+
+ /// Temporarily block signals and update zoom display
+ virtual void updateZoom() = 0;
+
+ /// The zoom display will get the keyboard focus.
+ virtual void letZoomGrabFocus() = 0;
+
+ /// Temporarily block signals and update rotation display
+ virtual void updateRotation() = 0;
+
+ virtual Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring&) = 0;
+
+ /// In auxiliary toolbox, set focus to widget having specific id
+ virtual void setToolboxFocusTo (const gchar *) = 0;
+
+ /// In auxiliary toolbox, set value of adjustment with specific id
+ virtual void setToolboxAdjustmentValue (const gchar *, double) = 0;
+
+ /// In auxiliary toolbox, return true if specific togglebutton is active
+ virtual bool isToolboxButtonActive (gchar const*) = 0;
+
+ /// Set the coordinate display
+ virtual void setCoordinateStatus (Geom::Point p) = 0;
+
+ /// Message widget will get no content
+ virtual void setMessage (Inkscape::MessageType type, gchar const* msg) = 0;
+
+
+ /** Show an info dialog with the given message */
+ virtual bool showInfoDialog( Glib::ustring const &message ) = 0;
+
+ /// Open yes/no dialog with warning text and confirmation question.
+ virtual bool warnDialog (Glib::ustring const &) = 0;
+
+ virtual Inkscape::UI::Widget::Dock* getDock () = 0;
+};
+
+} // namespace View
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_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/view/svg-view-widget.cpp b/src/ui/view/svg-view-widget.cpp
new file mode 100644
index 0000000..c349b5a
--- /dev/null
+++ b/src/ui/view/svg-view-widget.cpp
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A light-weight widget containing an SPCanvas for rendering an SVG.
+ */
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Includes code moved from svg-view.cpp authored by:
+ * MenTaLGuy
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * 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 <iostream>
+
+#include "svg-view-widget.h"
+
+#include "document.h"
+
+#include "2geom/transforms.h"
+
+#include "display/sp-canvas.h"
+#include "display/sp-canvas-group.h"
+#include "display/sp-canvas-item.h"
+#include "display/canvas-arena.h"
+
+#include "object/sp-item.h"
+#include "object/sp-root.h"
+
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+/**
+ * Callback connected with arena_event.
+ */
+// This hasn't worked since at least 0.48. It should result in a cursor change over <a></a> links.
+// There should be a better way of doing this. See note in canvas-arena.cpp.
+static gint arena_handler(SPCanvasArena */*arena*/, Inkscape::DrawingItem *ai,
+ GdkEvent *event, SVGViewWidget *svgview)
+{
+ static gdouble x, y;
+ static gboolean active = FALSE;
+ SPEvent spev;
+
+ SPItem *spitem = (ai) ? ai->getItem() : nullptr;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ active = TRUE;
+ x = event->button.x;
+ y = event->button.y;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+ if (active && (event->button.x == x) &&
+ (event->button.y == y)) {
+ spev.type = SPEvent::ACTIVATE;
+ if ( spitem != nullptr )
+ {
+ spitem->emitEvent (spev);
+ }
+ }
+ }
+ active = FALSE;
+ break;
+ case GDK_MOTION_NOTIFY:
+ active = FALSE;
+ break;
+ case GDK_ENTER_NOTIFY:
+ spev.type = SPEvent::MOUSEOVER;
+ spev.view = svgview;
+ if ( spitem != nullptr )
+ {
+ spitem->emitEvent (spev);
+ }
+ break;
+ case GDK_LEAVE_NOTIFY:
+ spev.type = SPEvent::MOUSEOUT;
+ spev.view = svgview;
+ if ( spitem != nullptr )
+ {
+ spitem->emitEvent (spev);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * A light-weight widget containing an SPCanvas for rendering an SVG.
+ * It's derived from a Gtk::ScrolledWindow like the previous C version, but that doesn't seem to be
+ * too useful.
+ */
+SVGViewWidget::SVGViewWidget(SPDocument* document)
+ : _document(nullptr)
+ , _dkey(0)
+ , _parent(nullptr)
+ , _drawing(nullptr)
+ , _hscale(1.0)
+ , _vscale(1.0)
+ , _rescale(false)
+ , _keepaspect(false)
+ , _width(0.0)
+ , _height(0.0)
+{
+ _canvas = SPCanvas::createAA();
+ add(*Glib::wrap(_canvas));
+
+ SPCanvasItem* item =
+ sp_canvas_item_new(SP_CANVAS(_canvas)->getRoot(), SP_TYPE_CANVAS_GROUP, nullptr);
+ _parent = SP_CANVAS_GROUP(item);
+
+ _drawing = sp_canvas_item_new (_parent, SP_TYPE_CANVAS_ARENA, nullptr);
+ g_signal_connect (G_OBJECT (_drawing), "arena_event", G_CALLBACK (arena_handler), this);
+
+ setDocument(document);
+
+ signal_size_allocate().connect(sigc::mem_fun(*this, &SVGViewWidget::size_allocate));
+}
+
+SVGViewWidget::~SVGViewWidget()
+{
+ if (_document) {
+ _document = nullptr;
+ }
+}
+
+void
+SVGViewWidget::setDocument(SPDocument* document)
+{
+ // Clear old document
+ if (_document) {
+ _document->getRoot()->invoke_hide(_dkey); // Removed from display tree
+ }
+
+ // Add new document
+ if (document) {
+ _document = document;
+
+ Inkscape::DrawingItem *ai = document->getRoot()->invoke_show(
+ SP_CANVAS_ARENA (_drawing)->drawing,
+ _dkey,
+ SP_ITEM_SHOW_DISPLAY);
+
+ if (ai) {
+ SP_CANVAS_ARENA (_drawing)->drawing.root()->prependChild(ai);
+ }
+
+ doRescale ();
+ }
+}
+
+void
+SVGViewWidget::setResize(int width, int height)
+{
+ // Triggers size_allocation which calls SVGViewWidget::size_allocate.
+ set_size_request(width, height);
+ queue_resize();
+}
+
+void
+SVGViewWidget::size_allocate(Gtk::Allocation& allocation)
+{
+ double width = allocation.get_width();
+ double height = allocation.get_height();
+
+ if (width < 0.0 || height < 0.0) {
+ std::cerr << "SVGViewWidget::size_allocate: negative dimensions!" << std::endl;
+ return;
+ }
+
+ _rescale = true;
+ _keepaspect = true;
+ _width = width;
+ _height = height;
+
+ doRescale ();
+}
+
+void
+SVGViewWidget::doRescale()
+{
+ if (!_document) {
+ std::cerr << "SVGViewWidget::doRescale: No document!" << std::endl;
+ return;
+ }
+
+ if (_document->getWidth().value("px") < 1e-9) {
+ std::cerr << "SVGViewWidget::doRescale: Width too small!" << std::endl;
+ return;
+ }
+
+ if (_document->getHeight().value("px") < 1e-9) {
+ std::cerr << "SVGViewWidget::doRescale: Height too small!" << std::endl;
+ return;
+ }
+
+ double x_offset = 0.0;
+ double y_offset = 0.0;
+ if (_rescale) {
+ _hscale = _width / _document->getWidth().value("px");
+ _vscale = _height / _document->getHeight().value("px");
+ if (_keepaspect) {
+ if (_hscale > _vscale) {
+ _hscale = _vscale;
+ x_offset = (_width - _document->getWidth().value("px") * _vscale)/2.0;
+ } else {
+ _vscale = _hscale;
+ y_offset = (_height - _document->getHeight().value("px") * _hscale)/2.0;
+ }
+ }
+ }
+
+ if (_drawing) {
+ sp_canvas_item_affine_absolute (_drawing, Geom::Scale(_hscale, _vscale) * Geom::Translate(x_offset, y_offset));
+ }
+}
+
+void
+SVGViewWidget::mouseover()
+{
+ GdkDisplay *display = gdk_display_get_default();
+ GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
+ GdkWindow *window = gtk_widget_get_window (GTK_WIDGET(SP_CANVAS_ITEM(_drawing)->canvas));
+ gdk_window_set_cursor(window, cursor);
+ g_object_unref(cursor);
+}
+
+void
+SVGViewWidget::mouseout()
+{
+ GdkWindow *window = gtk_widget_get_window (GTK_WIDGET(SP_CANVAS_ITEM(_drawing)->canvas));
+ gdk_window_set_cursor(window, nullptr);
+}
+
+} // Namespace View
+} // 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 :
diff --git a/src/ui/view/svg-view-widget.h b/src/ui/view/svg-view-widget.h
new file mode 100644
index 0000000..644c879
--- /dev/null
+++ b/src/ui/view/svg-view-widget.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A light-weight widget containing an SPCanvas with for rendering an SVG.
+ */
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * 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.
+ *
+ */
+
+#ifndef INKSCAPE_UI_SVG_VIEW_WIDGET_VARIATIONS_H
+#define INKSCAPE_UI_SVG_VIEW_WIDGET_VARIATIONS_H
+
+
+#include <gtkmm.h>
+
+class SPDocument;
+class SPCanvasGroup;
+class SPCanvasItem;
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+/**
+ * A light-weight widget containing an SPCanvas for rendering an SVG.
+ */
+class SVGViewWidget : public Gtk::ScrolledWindow {
+
+public:
+ SVGViewWidget(SPDocument* document);
+ ~SVGViewWidget() override;
+ void setDocument( SPDocument* document);
+ void setResize( int width, int height);
+
+private:
+ void size_allocate(Gtk::Allocation& allocation);
+
+ GtkWidget* _canvas;
+
+// From SVGView ---------------------------------
+
+public:
+ SPDocument* _document;
+ unsigned int _dkey;
+ SPCanvasGroup *_parent;
+ SPCanvasItem *_drawing;
+ double _hscale; ///< horizontal scale
+ double _vscale; ///< vertical scale
+ bool _rescale; ///< whether to rescale automatically
+ bool _keepaspect;
+ double _width;
+ double _height;
+
+ /**
+ * Helper function that sets rescale ratio.
+ */
+ void doRescale();
+
+ /**
+ * Change cursor (used for links).
+ */
+ void mouseover();
+ void mouseout();
+
+};
+
+} // Namespace View
+} // Namespace UI
+} // Namespace Inkscape
+
+#endif // INKSCAPE_UI_SVG_VIEW_WIDGET
+
+/*
+ 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 :
diff --git a/src/ui/view/view-widget.cpp b/src/ui/view/view-widget.cpp
new file mode 100644
index 0000000..ce9745d
--- /dev/null
+++ b/src/ui/view/view-widget.cpp
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "view.h"
+#include "view-widget.h"
+
+//using namespace Inkscape::UI::View;
+
+// SPViewWidget
+static void sp_view_widget_dispose(GObject *object);
+
+G_DEFINE_TYPE(SPViewWidget, sp_view_widget, GTK_TYPE_EVENT_BOX);
+
+/**
+ * Callback to initialize the SPViewWidget vtable.
+ */
+static void sp_view_widget_class_init(SPViewWidgetClass *vwc)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(vwc);
+
+ object_class->dispose = sp_view_widget_dispose;
+}
+
+/**
+ * Callback to initialize the SPViewWidget.
+ */
+static void sp_view_widget_init(SPViewWidget *vw)
+{
+ vw->view = nullptr;
+}
+
+/**
+ * Callback to disconnect from view and destroy SPViewWidget.
+ *
+ * Apparently, this gets only called when a desktop is closed, but then twice!
+ */
+static void sp_view_widget_dispose(GObject *object)
+{
+ SPViewWidget *vw = SP_VIEW_WIDGET(object);
+
+ if (vw->view) {
+ vw->view->close();
+ Inkscape::GC::release(vw->view);
+ vw->view = nullptr;
+ }
+
+ if (G_OBJECT_CLASS(sp_view_widget_parent_class)->dispose) {
+ G_OBJECT_CLASS(sp_view_widget_parent_class)->dispose(object);
+ }
+
+ Inkscape::GC::request_early_collection();
+}
+
+void sp_view_widget_set_view(SPViewWidget *vw, Inkscape::UI::View::View *view)
+{
+ g_return_if_fail(vw != nullptr);
+ g_return_if_fail(SP_IS_VIEW_WIDGET(vw));
+ g_return_if_fail(view != nullptr);
+
+ g_return_if_fail(vw->view == nullptr);
+
+ vw->view = view;
+ Inkscape::GC::anchor(view);
+
+ if (((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->set_view) {
+ ((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->set_view(vw, view);
+ }
+}
+
+bool sp_view_widget_shutdown(SPViewWidget *vw)
+{
+ g_return_val_if_fail(vw != nullptr, TRUE);
+ g_return_val_if_fail(SP_IS_VIEW_WIDGET(vw), TRUE);
+
+ if (((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->shutdown) {
+ return ((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->shutdown(vw);
+ }
+
+ return 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 :
diff --git a/src/ui/view/view-widget.h b/src/ui/view/view-widget.h
new file mode 100644
index 0000000..0296ac6
--- /dev/null
+++ b/src/ui/view/view-widget.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_VIEW_VIEWWIDGET_H
+#define INKSCAPE_UI_VIEW_VIEWWIDGET_H
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtk/gtk.h>
+
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+class View;
+} // namespace View
+} // namespace UI
+} // namespace Inkscape
+
+class SPViewWidget;
+class SPNamedView;
+
+#define SP_TYPE_VIEW_WIDGET (sp_view_widget_get_type ())
+#define SP_VIEW_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_VIEW_WIDGET, SPViewWidget))
+#define SP_VIEW_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_VIEW_WIDGET, SPViewWidgetClass))
+#define SP_IS_VIEW_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_VIEW_WIDGET))
+#define SP_IS_VIEW_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_VIEW_WIDGET))
+#define SP_VIEW_WIDGET_VIEW(w) (SP_VIEW_WIDGET (w)->view)
+#define SP_VIEW_WIDGET_DOCUMENT(w) (SP_VIEW_WIDGET (w)->view ? ((SPViewWidget *) (w))->view->doc : NULL)
+
+/**
+ * Registers the SPViewWidget class with Glib and returns its type number.
+ */
+GType sp_view_widget_get_type();
+
+/**
+ * Connects widget to view's 'resized' signal and calls virtual set_view()
+ * function.
+ */
+void sp_view_widget_set_view(SPViewWidget *vw, Inkscape::UI::View::View *view);
+
+/**
+ * Allows presenting 'save changes' dialog, FALSE - continue, TRUE - cancel.
+ * Calls the virtual shutdown() function of the SPViewWidget.
+ */
+bool sp_view_widget_shutdown(SPViewWidget *vw);
+
+/**
+ * SPViewWidget is a GUI widget that contain a single View. It is also
+ * an abstract base class with little functionality of its own.
+ */
+class SPViewWidget {
+ public:
+ GtkEventBox eventbox; // NOT USED!
+
+ Inkscape::UI::View::View *view;
+
+ // C++ Wrappers
+ GType getType() const {
+ return sp_view_widget_get_type();
+ }
+
+ void setView(Inkscape::UI::View::View *view) {
+ sp_view_widget_set_view(this, view);
+ }
+
+ gboolean shutdown() {
+ return sp_view_widget_shutdown(this);
+ }
+
+// void resized (double x, double y) = 0;
+};
+
+/**
+ * The Glib-style vtable for the SPViewWidget class.
+ */
+class SPViewWidgetClass {
+ public:
+ GtkEventBoxClass parent_class;
+
+ /* Virtual method to set/change/remove view */
+ void (* set_view) (SPViewWidget *vw, Inkscape::UI::View::View *view);
+ /// Virtual method about view size change
+ void (* view_resized) (SPViewWidget *vw, Inkscape::UI::View::View *view, gdouble width, gdouble height);
+
+ gboolean (* shutdown) (SPViewWidget *vw);
+};
+
+#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 :
diff --git a/src/ui/view/view.cpp b/src/ui/view/view.cpp
new file mode 100644
index 0000000..3b2b7f2
--- /dev/null
+++ b/src/ui/view/view.cpp
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <memory>
+#include "document.h"
+#include "view.h"
+#include "message-stack.h"
+#include "message-context.h"
+#include "verbs.h"
+#include "inkscape.h"
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+static void
+_onResized (double x, double y, View* v)
+{
+ v->onResized (x,y);
+}
+
+static void
+_onRedrawRequested (View* v)
+{
+ v->onRedrawRequested();
+}
+
+static void
+_onStatusMessage (Inkscape::MessageType type, gchar const *message, View* v)
+{
+ v->onStatusMessage (type, message);
+}
+
+static void
+_onDocumentURISet (gchar const* uri, View* v)
+{
+ v->onDocumentURISet (uri);
+}
+
+static void
+_onDocumentResized (double x, double y, View* v)
+{
+ v->onDocumentResized (x,y);
+}
+
+//--------------------------------------------------------------------
+View::View()
+: _doc(nullptr)
+{
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _tips_message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack));
+
+ _resized_connection = _resized_signal.connect (sigc::bind (sigc::ptr_fun (&_onResized), this));
+ _redraw_requested_connection = _redraw_requested_signal.connect (sigc::bind (sigc::ptr_fun (&_onRedrawRequested), this));
+
+ _message_changed_connection = _message_stack->connectChanged (sigc::bind (sigc::ptr_fun (&_onStatusMessage), this));
+}
+
+View::~View()
+{
+ _close();
+}
+
+void View::_close() {
+ _message_changed_connection.disconnect();
+
+ _tips_message_context = nullptr;
+
+ _message_stack = nullptr;
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ _document_resized_connection.disconnect();
+ if (INKSCAPE.remove_document(_doc)) {
+ // this was the last view of this document, so delete it
+ // delete _doc; Delete now handled in Inkscape::Application
+ }
+ _doc = nullptr;
+ }
+
+ Inkscape::Verb::delete_all_view (this);
+}
+
+void View::emitResized (double width, double height)
+{
+ _resized_signal.emit (width, height);
+}
+
+void View::requestRedraw()
+{
+ _redraw_requested_signal.emit();
+}
+
+void View::setDocument(SPDocument *doc) {
+ g_return_if_fail(doc != nullptr);
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ _document_resized_connection.disconnect();
+ if (INKSCAPE.remove_document(_doc)) {
+ // this was the last view of this document, so delete it
+ // delete _doc; Delete now handled in Inkscape::Application
+ }
+ }
+
+ INKSCAPE.add_document(doc);
+
+ _doc = doc;
+ _document_uri_set_connection =
+ _doc->connectURISet(sigc::bind(sigc::ptr_fun(&_onDocumentURISet), this));
+ _document_resized_connection =
+ _doc->connectResized(sigc::bind(sigc::ptr_fun(&_onDocumentResized), this));
+ _document_uri_set_signal.emit( _doc->getDocumentURI() );
+}
+
+}}}
+
+/*
+ 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/view/view.h b/src/ui/view/view.h
new file mode 100644
index 0000000..3acfd2c
--- /dev/null
+++ b/src/ui/view/view.h
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_VIEW_VIEW_H
+#define INKSCAPE_UI_VIEW_VIEW_H
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+#include <cstddef>
+#include <memory>
+#include <sigc++/connection.h>
+#include "message.h"
+#include "inkgc/gc-managed.h"
+#include "gc-finalized.h"
+#include "gc-anchored.h"
+#include <2geom/forward.h>
+
+/**
+ * Iterates until true or returns false.
+ * When used as signal accumulator, stops emission if one slot returns true.
+ */
+struct StopOnTrue {
+ typedef bool result_type;
+
+ template<typename T_iterator>
+ result_type operator()(T_iterator first, T_iterator last) const{
+ for (; first != last; ++first)
+ if (*first) return true;
+ return false;
+ }
+};
+
+/**
+ * Iterates until nonzero or returns 0.
+ * When used as signal accumulator, stops emission if one slot returns nonzero.
+ */
+struct StopOnNonZero {
+ typedef int result_type;
+
+ template<typename T_iterator>
+ result_type operator()(T_iterator first, T_iterator last) const{
+ for (; first != last; ++first)
+ if (*first) return *first;
+ return 0;
+ }
+};
+
+class SPDocument;
+
+namespace Inkscape {
+ class MessageContext;
+ class MessageStack;
+ namespace UI {
+ namespace View {
+
+/**
+ * View is an abstract base class of all UI document views. This
+ * includes both the editing window and the SVG preview, but does not
+ * include the non-UI RGBA buffer-based Inkscape::Drawing nor the XML editor or
+ * similar views. The View base class has very little functionality of
+ * its own.
+ */
+class View : public GC::Managed<>,
+ public GC::Finalized,
+ public GC::Anchored
+{
+public:
+
+ View();
+
+ /**
+ * Deletes and nulls all View message stacks and disconnects it from signals.
+ */
+ ~View() override;
+
+ void close() { _close(); }
+
+ /// Returns a pointer to the view's document.
+ SPDocument *doc() const
+ { return _doc; }
+ /// Returns a pointer to the view's message stack.
+ std::shared_ptr<Inkscape::MessageStack> messageStack() const
+ { return _message_stack; }
+ /// Returns a pointer to the view's tipsMessageContext.
+ Inkscape::MessageContext *tipsMessageContext() const
+ { return _tips_message_context.get(); }
+
+ void emitResized(gdouble width, gdouble height);
+ void requestRedraw();
+
+ virtual void onResized (double, double) {};
+ virtual void onRedrawRequested() {};
+ virtual void onStatusMessage (Inkscape::MessageType type, gchar const *message) {};
+ virtual void onDocumentURISet (gchar const* uri) {};
+ virtual void onDocumentResized (double, double) {};
+ virtual bool shutdown() { return false; };
+
+protected:
+ SPDocument *_doc;
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _tips_message_context;
+
+ virtual void _close();
+
+ /**
+ * Disconnects the view from the document signals, connects the view
+ * to a new one, and emits the _document_set_signal on the view.
+ *
+ * This is code common to all subclasses and called from their
+ * setDocument() methods after they are done.
+ *
+ * @param doc The new document to connect the view to.
+ */
+ virtual void setDocument(SPDocument *doc);
+
+ sigc::signal<void,double,double> _resized_signal;
+ sigc::signal<void,gchar const*> _document_uri_set_signal;
+ sigc::signal<void> _redraw_requested_signal;
+
+private:
+ sigc::connection _resized_connection;
+ sigc::connection _redraw_requested_connection;
+ sigc::connection _message_changed_connection; // foreign
+ sigc::connection _document_uri_set_connection; // foreign
+ sigc::connection _document_resized_connection; // foreign
+};
+
+}}}
+
+#endif // INKSCAPE_UI_VIEW_VIEW_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/widget/alignment-selector.cpp b/src/ui/widget/alignment-selector.cpp
new file mode 100644
index 0000000..e5ac17a
--- /dev/null
+++ b/src/ui/widget/alignment-selector.cpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.cpp
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/alignment-selector.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void AlignmentSelector::setupButton(const Glib::ustring& icon, Gtk::Button& button) {
+ Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ buttonIcon->show();
+
+ button.set_relief(Gtk::RELIEF_NONE);
+ button.show();
+ button.add(*buttonIcon);
+ button.set_can_focus(false);
+}
+
+AlignmentSelector::AlignmentSelector()
+ : _container()
+{
+ set_halign(Gtk::ALIGN_CENTER);
+ setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]);
+ setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]);
+ setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]);
+ setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]);
+ setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]);
+ setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]);
+
+ _container.set_row_homogeneous();
+ _container.set_column_homogeneous(true);
+
+ for(int i = 0; i < 9; ++i) {
+ _buttons[i].signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &AlignmentSelector::btn_activated), i));
+
+ _container.attach(_buttons[i], i % 3, i / 3, 1, 1);
+ }
+
+ this->add(_container);
+}
+
+AlignmentSelector::~AlignmentSelector()
+{
+ // TODO Auto-generated destructor stub
+}
+
+void AlignmentSelector::btn_activated(int index)
+{
+ _alignmentClicked.emit(index);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/alignment-selector.h b/src/ui/widget/alignment-selector.h
new file mode 100644
index 0000000..5bcf0fa
--- /dev/null
+++ b/src/ui/widget/alignment-selector.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/button.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlignmentSelector : public Gtk::Bin
+{
+private:
+ Gtk::Button _buttons[9];
+ Gtk::Grid _container;
+
+ sigc::signal<void, int> _alignmentClicked;
+
+ void setupButton(const Glib::ustring &icon, Gtk::Button &button);
+ void btn_activated(int index);
+
+public:
+
+ sigc::signal<void, int> &on_alignmentClicked() { return _alignmentClicked; }
+
+ AlignmentSelector();
+ ~AlignmentSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.cpp b/src/ui/widget/anchor-selector.cpp
new file mode 100644
index 0000000..b151a81
--- /dev/null
+++ b/src/ui/widget/anchor-selector.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.cpp
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include "ui/widget/anchor-selector.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void AnchorSelector::setupButton(const Glib::ustring& icon, Gtk::ToggleButton& button) {
+ Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ buttonIcon->show();
+
+ button.set_relief(Gtk::RELIEF_NONE);
+ button.show();
+ button.add(*buttonIcon);
+ button.set_can_focus(false);
+}
+
+AnchorSelector::AnchorSelector()
+ : _container()
+{
+ set_halign(Gtk::ALIGN_CENTER);
+ setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]);
+ setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]);
+ setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]);
+ setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]);
+ setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]);
+ setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]);
+
+ _container.set_row_homogeneous();
+ _container.set_column_homogeneous(true);
+
+ for (int i = 0; i < 9; ++i) {
+ _buttons[i].signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &AnchorSelector::btn_activated), i));
+
+ _container.attach(_buttons[i], i % 3, i / 3, 1, 1);
+ }
+ _selection = 4;
+ _buttons[4].set_active();
+
+ this->add(_container);
+}
+
+AnchorSelector::~AnchorSelector()
+{
+ // TODO Auto-generated destructor stub
+}
+
+void AnchorSelector::btn_activated(int index)
+{
+ if (_selection == index && _buttons[index].get_active() == false) {
+ _buttons[index].set_active(true);
+ }
+ else if (_selection != index && _buttons[index].get_active()) {
+ int old_selection = _selection;
+ _selection = index;
+ _buttons[old_selection].set_active(false);
+ _selectionChanged.emit();
+ }
+}
+
+void AnchorSelector::setAlignment(int horizontal, int vertical)
+{
+ int index = 3 * vertical + horizontal;
+ if (index >= 0 && index < 9) {
+ _buttons[index].set_active(!_buttons[index].get_active());
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.h b/src/ui/widget/anchor-selector.h
new file mode 100644
index 0000000..49ce0b2
--- /dev/null
+++ b/src/ui/widget/anchor-selector.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AnchorSelector : public Gtk::Bin
+{
+private:
+ Gtk::ToggleButton _buttons[9];
+ int _selection;
+ Gtk::Grid _container;
+
+ sigc::signal<void> _selectionChanged;
+
+ void setupButton(const Glib::ustring &icon, Gtk::ToggleButton &button);
+ void btn_activated(int index);
+
+public:
+
+ int getHorizontalAlignment() { return _selection % 3; }
+ int getVerticalAlignment() { return _selection / 3; }
+
+ sigc::signal<void> &on_selectionChanged() { return _selectionChanged; }
+
+ void setAlignment(int horizontal, int vertical);
+
+ AnchorSelector();
+ ~AnchorSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/attr-widget.h b/src/ui/widget/attr-widget.h
new file mode 100644
index 0000000..014a540
--- /dev/null
+++ b/src/ui/widget/attr-widget.h
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ATTR_WIDGET_H
+#define INKSCAPE_UI_WIDGET_ATTR_WIDGET_H
+
+#include "attributes.h"
+#include "object/sp-object.h"
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum DefaultValueType
+{
+ T_NONE,
+ T_DOUBLE,
+ T_VECT_DOUBLE,
+ T_BOOL,
+ T_UINT,
+ T_CHARPTR
+};
+
+/**
+ * Very basic interface for classes that control attributes.
+ */
+class DefaultValueHolder
+{
+ DefaultValueType type;
+ union {
+ double d_val;
+ std::vector<double>* vt_val;
+ bool b_val;
+ unsigned int uint_val;
+ char* cptr_val;
+ } value;
+
+ //FIXME remove copy ctor and assignment operator as private to avoid double free of the vector
+public:
+ DefaultValueHolder () {
+ type = T_NONE;
+ }
+
+ DefaultValueHolder (double d) {
+ type = T_DOUBLE;
+ value.d_val = d;
+ }
+
+ DefaultValueHolder (std::vector<double>* d) {
+ type = T_VECT_DOUBLE;
+ value.vt_val = d;
+ }
+
+ DefaultValueHolder (char* c) {
+ type = T_CHARPTR;
+ value.cptr_val = c;
+ }
+
+ DefaultValueHolder (bool d) {
+ type = T_BOOL;
+ value.b_val = d;
+ }
+
+ DefaultValueHolder (unsigned int ui) {
+ type = T_UINT;
+ value.uint_val = ui;
+ }
+
+ ~DefaultValueHolder() {
+ if (type == T_VECT_DOUBLE)
+ delete value.vt_val;
+ }
+
+ unsigned int as_uint() {
+ g_assert (type == T_UINT);
+ return value.uint_val;
+ }
+
+ bool as_bool() {
+ g_assert (type == T_BOOL);
+ return value.b_val;
+ }
+
+ double as_double() {
+ g_assert (type == T_DOUBLE);
+ return value.d_val;
+ }
+
+ std::vector<double>* as_vector() {
+ g_assert (type == T_VECT_DOUBLE);
+ return value.vt_val;
+ }
+
+ char* as_charptr() {
+ g_assert (type == T_CHARPTR);
+ return value.cptr_val;
+ }
+};
+
+class AttrWidget
+{
+public:
+ AttrWidget(const SPAttributeEnum a, unsigned int value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttributeEnum a, double value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttributeEnum a, bool value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttributeEnum a, char* value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttributeEnum a)
+ : _attr(a),
+ _default()
+ {}
+
+ virtual ~AttrWidget()
+ = default;
+
+ virtual Glib::ustring get_as_attribute() const = 0;
+ virtual void set_from_attribute(SPObject*) = 0;
+
+ SPAttributeEnum get_attribute() const
+ {
+ return _attr;
+ }
+
+ sigc::signal<void>& signal_attr_changed()
+ {
+ return _signal;
+ }
+protected:
+ DefaultValueHolder* get_default() { return &_default; }
+ const gchar* attribute_value(SPObject* o) const
+ {
+ const gchar* name = (const gchar*)sp_attribute_name(_attr);
+ if(name && o) {
+ const gchar* val = o->getRepr()->attribute(name);
+ return val;
+ }
+ return nullptr;
+ }
+
+private:
+ const SPAttributeEnum _attr;
+ DefaultValueHolder _default;
+ sigc::signal<void> _signal;
+};
+
+}
+}
+}
+
+#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/widget/button.cpp b/src/ui/widget/button.cpp
new file mode 100644
index 0000000..f119c06
--- /dev/null
+++ b/src/ui/widget/button.cpp
@@ -0,0 +1,273 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic button widget
+ *//*
+ * Authors:
+ * see git history
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm.h>
+
+#include "button.h"
+#include "helper/action-context.h"
+#include "helper/action.h"
+#include "shortcuts.h"
+#include "ui/icon-loader.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Button::~Button()
+{
+ if (_action) {
+ _c_set_active.disconnect();
+ _c_set_sensitive.disconnect();
+ g_object_unref(_action);
+ }
+
+ if (_doubleclick_action) {
+ set_doubleclick_action(nullptr);
+ }
+}
+
+void
+Button::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ auto child = get_child();
+
+ if (child) {
+ child->get_preferred_width(minimal_width, natural_width);
+ } else {
+ minimal_width = 0;
+ natural_width = 0;
+ }
+
+ auto context = get_style_context();
+
+ auto padding = context->get_padding(context->get_state());
+ auto border = context->get_border(context->get_state());
+
+ minimal_width += MAX(2, padding.get_left() + padding.get_right() + border.get_left() + border.get_right());
+ natural_width += MAX(2, padding.get_left() + padding.get_right() + border.get_left() + border.get_right());
+}
+
+void
+Button::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ auto child = get_child();
+
+ if (child) {
+ child->get_preferred_height(minimal_height, natural_height);
+ } else {
+ minimal_height = 0;
+ natural_height = 0;
+ }
+
+ auto context = get_style_context();
+
+ auto padding = context->get_padding(context->get_state());
+ auto border = context->get_border(context->get_state());
+
+ minimal_height += MAX(2, padding.get_top() + padding.get_bottom() + border.get_top() + border.get_bottom());
+ natural_height += MAX(2, padding.get_top() + padding.get_bottom() + border.get_top() + border.get_bottom());
+}
+
+void
+Button::on_clicked()
+{
+ if (_type == BUTTON_TYPE_TOGGLE) {
+ Gtk::Button::on_clicked();
+ }
+}
+
+bool
+Button::process_event(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (_doubleclick_action) {
+ sp_action_perform(_doubleclick_action, nullptr);
+ }
+ return true;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void
+Button::perform_action()
+{
+ if (_action) {
+ sp_action_perform(_action, nullptr);
+ }
+}
+
+Button::Button(GtkIconSize size,
+ ButtonType type,
+ SPAction *action,
+ SPAction *doubleclick_action)
+ :
+ _action(nullptr),
+ _doubleclick_action(nullptr),
+ _type(type),
+ _lsize(CLAMP(size, GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_DIALOG))
+{
+ set_border_width(0);
+
+ set_can_focus(false);
+ set_can_default(false);
+
+ _on_clicked = signal_clicked().connect(sigc::mem_fun(*this, &Button::perform_action));
+
+ signal_event().connect(sigc::mem_fun(*this, &Button::process_event));
+
+ set_action(action);
+
+ if (doubleclick_action) {
+ set_doubleclick_action(doubleclick_action);
+ }
+
+ // The Inkscape style is no-relief buttons
+ set_relief(Gtk::RELIEF_NONE);
+}
+
+void
+Button::toggle_set_down(bool down)
+{
+ _on_clicked.block();
+ set_active(down);
+ _on_clicked.unblock();
+}
+
+void
+Button::set_doubleclick_action(SPAction *action)
+{
+ if (_doubleclick_action) {
+ g_object_unref(_doubleclick_action);
+ }
+ _doubleclick_action = action;
+ if (action) {
+ g_object_ref(action);
+ }
+}
+
+void
+Button::set_action(SPAction *action)
+{
+ Gtk::Widget *child;
+
+ if (_action) {
+ _c_set_active.disconnect();
+ _c_set_sensitive.disconnect();
+ child = get_child();
+ if (child) {
+ remove();
+ }
+ g_object_unref(_action);
+ }
+
+ _action = action;
+ if (action) {
+ g_object_ref(action);
+ _c_set_active = action->signal_set_active.connect(
+ sigc::mem_fun(*this, &Button::action_set_active));
+
+ _c_set_sensitive = action->signal_set_sensitive.connect(
+ sigc::mem_fun(*this, &Gtk::Widget::set_sensitive));
+
+ if (action->image) {
+ child = Glib::wrap(sp_get_icon_image(action->image, _lsize));
+ child->show();
+ add(*child);
+ }
+ }
+
+ set_composed_tooltip(action);
+}
+
+void
+Button::action_set_active(bool active)
+{
+ if (_type != BUTTON_TYPE_TOGGLE) {
+ return;
+ }
+
+ /* temporarily lobotomized until SPActions are per-view */
+ if (false && !active != !get_active()) {
+ toggle_set_down(active);
+ }
+}
+
+void
+Button::set_composed_tooltip(SPAction *action)
+{
+ if (action) {
+ unsigned int shortcut = sp_shortcut_get_primary(action->verb);
+ if (shortcut != GDK_KEY_VoidSymbol) {
+ // there's both action and shortcut
+
+ gchar *key = sp_shortcut_get_label(shortcut);
+
+ gchar *tip = g_strdup_printf("%s (%s)", action->tip, key);
+ set_tooltip_text(tip);
+ g_free(tip);
+ g_free(key);
+ } else {
+ // action has no shortcut
+ set_tooltip_text(action->tip);
+ }
+ } else {
+ // no action
+ set_tooltip_text(nullptr);
+ }
+}
+
+Button::Button(GtkIconSize size,
+ ButtonType type,
+ Inkscape::UI::View::View *view,
+ const gchar *name,
+ const gchar *tip)
+ :
+ _action(nullptr),
+ _doubleclick_action(nullptr),
+ _type(type),
+ _lsize(CLAMP(size, GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_DIALOG))
+{
+ set_border_width(0);
+
+ set_can_focus(false);
+ set_can_default(false);
+
+ _on_clicked = signal_clicked().connect(sigc::mem_fun(*this, &Button::perform_action));
+ signal_event().connect(sigc::mem_fun(*this, &Button::process_event));
+
+ auto action = sp_action_new(Inkscape::ActionContext(view), name, name, tip, name, nullptr);
+ set_action(action);
+ g_object_unref(action);
+}
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/button.h b/src/ui/widget/button.h
new file mode 100644
index 0000000..0b1bfc2
--- /dev/null
+++ b/src/ui/widget/button.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic button widget
+ *//*
+ * Authors:
+ * see git history
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_BUTTON_H
+#define SEEN_SP_BUTTON_H
+
+#include <gtkmm/togglebutton.h>
+#include <sigc++/connection.h>
+
+struct SPAction;
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+class View;
+}
+
+namespace Widget {
+
+enum ButtonType {
+ BUTTON_TYPE_NORMAL,
+ BUTTON_TYPE_TOGGLE
+};
+
+class Button : public Gtk::ToggleButton{
+private:
+ ButtonType _type;
+ GtkIconSize _lsize;
+ unsigned int _psize;
+ SPAction *_action;
+ SPAction *_doubleclick_action;
+
+ sigc::connection _c_set_active;
+ sigc::connection _c_set_sensitive;
+
+ void set_action(SPAction *action);
+ void set_doubleclick_action(SPAction *action);
+ void set_composed_tooltip(SPAction *action);
+ void action_set_active(bool active);
+ void perform_action();
+ bool process_event(GdkEvent *event);
+
+ sigc::connection _on_clicked;
+
+protected:
+ void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
+ void on_clicked() override;
+
+public:
+ Button(GtkIconSize size,
+ ButtonType type,
+ SPAction *action,
+ SPAction *doubleclick_action);
+
+ Button(GtkIconSize size,
+ ButtonType type,
+ Inkscape::UI::View::View *view,
+ const gchar *name,
+ const gchar *tip);
+
+ ~Button() override;
+
+ void toggle_set_down(bool down);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#endif // !SEEN_SP_BUTTON_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 :
diff --git a/src/ui/widget/clipmaskicon.cpp b/src/ui/widget/clipmaskicon.cpp
new file mode 100644
index 0000000..1093c1f
--- /dev/null
+++ b/src/ui/widget/clipmaskicon.cpp
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/clipmaskicon.h"
+
+#include "layertypeicon.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "widgets/toolbox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ClipMaskIcon::ClipMaskIcon() :
+ Glib::ObjectBase(typeid(ClipMaskIcon)),
+ Gtk::CellRendererPixbuf(),
+ _pixClipName(INKSCAPE_ICON("path-cut")),
+ _pixMaskName(INKSCAPE_ICON("path-difference")),
+ _pixBothName(INKSCAPE_ICON("bitmap-trace")),
+ _property_active(*this, "active", 0),
+ _property_pixbuf_clip(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_mask(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_both(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+{
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ _property_pixbuf_clip = sp_get_icon_pixbuf(_pixClipName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_mask = sp_get_icon_pixbuf(_pixMaskName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_both = sp_get_icon_pixbuf(_pixBothName, GTK_ICON_SIZE_MENU);
+
+ property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+}
+
+void ClipMaskIcon::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void ClipMaskIcon::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void ClipMaskIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ switch (_property_active.get_value())
+ {
+ case 1:
+ property_pixbuf() = _property_pixbuf_clip;
+ break;
+ case 2:
+ property_pixbuf() = _property_pixbuf_mask;
+ break;
+ case 3:
+ property_pixbuf() = _property_pixbuf_both;
+ break;
+ default:
+ property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+ break;
+ }
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool ClipMaskIcon::activate_vfunc(GdkEvent* /*event*/,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& /*path*/,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ return false;
+}
+
+
+} // namespace Widget
+} // 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/widget/clipmaskicon.h b/src/ui/widget/clipmaskicon.h
new file mode 100644
index 0000000..d8bbe52
--- /dev/null
+++ b/src/ui/widget/clipmaskicon.h
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_CLIPMASKICON_H__
+#define __UI_DIALOG_CLIPMASKICON_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ClipMaskIcon : public Gtk::CellRendererPixbuf {
+public:
+ ClipMaskIcon();
+ ~ClipMaskIcon() override = default;;
+
+ Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ int phys;
+
+ Glib::ustring _pixClipName;
+ Glib::ustring _pixMaskName;
+ Glib::ustring _pixBothName;
+
+ Glib::Property<int> _property_active;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_clip;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_mask;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_both;
+
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/color-entry.cpp b/src/ui/widget/color-entry.cpp
new file mode 100644
index 0000000..804350c
--- /dev/null
+++ b/src/ui/widget/color-entry.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+
+#include "color-entry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorEntry::ColorEntry(SelectedColor &color)
+ : _color(color)
+ , _updating(false)
+ , _updatingrgba(false)
+ , _prevpos(0)
+ , _lastcolor(0)
+{
+ _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ signal_activate().connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ get_buffer()->signal_inserted_text().connect(sigc::mem_fun(this, &ColorEntry::_inputCheck));
+ _onColorChanged();
+
+ // add extra character for pasting a hash, '#11223344'
+ set_max_length(9);
+ set_width_chars(8);
+ set_tooltip_text(_("Hexadecimal RGBA value of the color"));
+}
+
+ColorEntry::~ColorEntry()
+{
+ _color_changed_connection.disconnect();
+ _color_dragged_connection.disconnect();
+}
+
+void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars)
+{
+ // remember position of last character, so we can remove it.
+ // we only overflow by 1 character at most.
+ _prevpos = pos + n_chars - 1;
+}
+
+void ColorEntry::on_changed()
+{
+ if (_updating) {
+ return;
+ }
+ if (_updatingrgba) {
+ return; // Typing text into entry box
+ }
+
+ Glib::ustring text = get_text();
+ bool changed = false;
+
+ // Coerce the value format to hexadecimal
+ for (auto it = text.begin(); it != text.end(); /*++it*/) {
+ if (!g_ascii_isxdigit(*it)) {
+ text.erase(it);
+ changed = true;
+ } else {
+ ++it;
+ }
+ }
+
+ if (text.size() > 8) {
+ text.erase(_prevpos, 1);
+ changed = true;
+ }
+
+ // autofill rules
+ gchar *str = g_strdup(text.c_str());
+ gchar *end = nullptr;
+ guint64 rgba = g_ascii_strtoull(str, &end, 16);
+ ptrdiff_t len = end - str;
+ if (len < 8) {
+ if (len == 0) {
+ rgba = _lastcolor;
+ } else if (len <= 2) {
+ if (len == 1) {
+ rgba *= 17;
+ }
+ rgba = (rgba << 24) + (rgba << 16) + (rgba << 8);
+ } else if (len <= 4) {
+ // display as rrggbbaa
+ rgba = rgba << (4 * (4 - len));
+ guint64 r = rgba & 0xf000;
+ guint64 g = rgba & 0x0f00;
+ guint64 b = rgba & 0x00f0;
+ guint64 a = rgba & 0x000f;
+ rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a);
+ } else {
+ rgba = rgba << (4 * (8 - len));
+ }
+
+ if (len == 7) {
+ rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f);
+ } else if (len == 5) {
+ rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff);
+ } else if (len != 4 && len != 8) {
+ rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff);
+ }
+ }
+
+ _updatingrgba = true;
+ if (changed) {
+ set_text(str);
+ }
+ SPColor color(rgba);
+ _color.setColorAlpha(color, SP_RGBA32_A_F(rgba));
+ _updatingrgba = false;
+
+ g_free(str);
+}
+
+
+void ColorEntry::_onColorChanged()
+{
+ if (_updatingrgba) {
+ return;
+ }
+
+ SPColor color = _color.color();
+ gdouble alpha = _color.alpha();
+
+ _lastcolor = color.toRGBA32(alpha);
+ Glib::ustring text = Glib::ustring::format(std::hex, std::setw(8), std::setfill(L'0'), _lastcolor);
+
+ Glib::ustring old_text = get_text();
+ if (old_text != text) {
+ _updating = true;
+ set_text(text);
+ _updating = 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/widget/color-entry.h b/src/ui/widget/color-entry.h
new file mode 100644
index 0000000..4df80de
--- /dev/null
+++ b/src/ui/widget/color-entry.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_ENTRY_H
+#define SEEN_COLOR_ENTRY_H
+
+#include <gtkmm/entry.h>
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorEntry : public Gtk::Entry
+{
+public:
+ ColorEntry(SelectedColor &color);
+ ~ColorEntry() override;
+
+protected:
+ void on_changed() override;
+
+private:
+ void _onColorChanged();
+ void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/);
+
+ SelectedColor &_color;
+ sigc::connection _color_changed_connection;
+ sigc::connection _color_dragged_connection;
+ bool _updating;
+ bool _updatingrgba;
+ guint32 _lastcolor;
+ int _prevpos;
+};
+
+}
+}
+}
+
+#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/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp
new file mode 100644
index 0000000..3100605
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.cpp
@@ -0,0 +1,1074 @@
+// 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.
+ */
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <set>
+#include <utility>
+
+#include <gtkmm/adjustment.h>
+#include <glibmm/i18n.h>
+
+#include "colorspace.h"
+#include "document.h"
+#include "inkscape.h"
+#include "profile-manager.h"
+
+#include "svg/svg-icc-color.h"
+
+#include "ui/dialog-events.h"
+#include "ui/util.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+
+#define noDEBUG_LCMS
+
+#if defined(HAVE_LIBLCMS2)
+#include "object/color-profile.h"
+#include "cms-system.h"
+#include "color-profile-cms-fns.h"
+
+#ifdef DEBUG_LCMS
+#include "preferences.h"
+#endif // DEBUG_LCMS
+#endif // defined(HAVE_LIBLCMS2)
+
+#ifdef DEBUG_LCMS
+extern guint update_in_progress;
+#define DEBUG_MESSAGE(key, ...) \
+ { \
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); \
+ bool dump = prefs->getBool("/options/scislac/" #key); \
+ bool dumpD = prefs->getBool("/options/scislac/" #key "D"); \
+ bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2"); \
+ dumpD && = ((update_in_progress == 0) || dumpD2); \
+ if (dump) { \
+ g_message(__VA_ARGS__); \
+ } \
+ if (dumpD) { \
+ GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, \
+ GTK_BUTTONS_OK, __VA_ARGS__); \
+ g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog); \
+ gtk_widget_show_all(dialog); \
+ } \
+ }
+#endif // DEBUG_LCMS
+
+
+#define XPAD 4
+#define YPAD 1
+
+namespace {
+
+size_t maxColorspaceComponentCount = 0;
+
+#if defined(HAVE_LIBLCMS2)
+
+/**
+ * Internal variable to track all known colorspaces.
+ */
+std::set<cmsUInt32Number> knownColorspaces;
+
+#endif
+
+/**
+ * Helper function to handle GTK2/GTK3 attachment #ifdef code.
+ */
+void attachToGridOrTable(GtkWidget *parent, GtkWidget *child, guint left, guint top, guint width, guint height,
+ bool hexpand = false, bool centered = false, guint xpadding = XPAD, guint ypadding = YPAD)
+{
+ gtk_widget_set_margin_start(child, xpadding);
+ gtk_widget_set_margin_end(child, xpadding);
+ gtk_widget_set_margin_top(child, ypadding);
+ gtk_widget_set_margin_bottom(child, ypadding);
+
+ if (hexpand) {
+ gtk_widget_set_hexpand(child, TRUE);
+ }
+
+ if (centered) {
+ gtk_widget_set_halign(child, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(child, GTK_ALIGN_CENTER);
+ }
+
+ gtk_grid_attach(GTK_GRID(parent), child, left, top, width, height);
+}
+
+} // namespace
+
+/*
+icSigRgbData
+icSigCmykData
+icSigCmyData
+*/
+#define SPACE_ID_RGB 0
+#define SPACE_ID_CMY 1
+#define SPACE_ID_CMYK 2
+
+
+colorspace::Component::Component()
+ : name()
+ , tip()
+ , scale(1)
+{
+}
+
+colorspace::Component::Component(std::string name, std::string tip, guint scale)
+ : name(std::move(name))
+ , tip(std::move(tip))
+ , scale(scale)
+{
+}
+
+#if defined(HAVE_LIBLCMS2)
+static cmsUInt16Number *getScratch()
+{
+ // bytes per pixel * input channels * width
+ static cmsUInt16Number *scritch = static_cast<cmsUInt16Number *>(g_new(cmsUInt16Number, 4 * 1024));
+
+ return scritch;
+}
+
+std::vector<colorspace::Component> colorspace::getColorSpaceInfo(uint32_t space)
+{
+ static std::map<cmsUInt32Number, std::vector<Component> > sets;
+ if (sets.empty()) {
+ sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16
+ sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1));
+ sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2));
+
+ sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16
+ sets[cmsSigLabData].push_back(Component("_a", "a", 256));
+ sets[cmsSigLabData].push_back(Component("_b", "b", 256));
+
+ // cmsSigLuvData
+
+ sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16
+ sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1));
+ sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1));
+
+ sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16
+ sets[cmsSigYxyData].push_back(Component("_x", "x", 1));
+ sets[cmsSigYxyData].push_back(Component("y", "y", 1));
+
+ sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16
+ sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1));
+ sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1));
+
+ sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16
+
+ sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16
+ sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1));
+ sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1));
+
+ sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16
+ sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1));
+ sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1));
+
+ sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16
+ sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1));
+ sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1));
+ sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1));
+
+ sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16
+ sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1));
+ sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1));
+
+ for (auto & set : sets) {
+ knownColorspaces.insert(set.first);
+ maxColorspaceComponentCount = std::max(maxColorspaceComponentCount, set.second.size());
+ }
+ }
+
+ std::vector<Component> target;
+
+ if (sets.find(space) != sets.end()) {
+ target = sets[space];
+ }
+ return target;
+}
+
+
+std::vector<colorspace::Component> colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof)
+{
+ return getColorSpaceInfo(asICColorSpaceSig(prof->getColorSpace()));
+}
+
+#endif // defined(HAVE_LIBLCMS2)
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Class containing the parts for a single color component's UI presence.
+ */
+class ComponentUI {
+ public:
+ ComponentUI()
+ : _component()
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _btn(nullptr)
+ , _label(nullptr)
+ , _map(nullptr)
+ {
+ }
+
+ ComponentUI(colorspace::Component component)
+ : _component(std::move(component))
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _btn(nullptr)
+ , _label(nullptr)
+ , _map(nullptr)
+ {
+ }
+
+ colorspace::Component _component;
+ GtkAdjustment *_adj; // Component adjustment
+ Inkscape::UI::Widget::ColorSlider *_slider;
+ GtkWidget *_btn; // spinbutton
+ GtkWidget *_label; // Label
+ guchar *_map;
+};
+
+/**
+ * Class that implements the internals of the selector.
+ */
+class ColorICCSelectorImpl {
+ public:
+ ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color);
+
+ ~ColorICCSelectorImpl();
+
+ static void _adjustmentChanged(GtkAdjustment *adjustment, ColorICCSelectorImpl *cs);
+
+ void _sliderGrabbed();
+ void _sliderReleased();
+ void _sliderChanged();
+
+ static void _profileSelected(GtkWidget *src, gpointer data);
+ static void _fixupHit(GtkWidget *src, gpointer data);
+
+#if defined(HAVE_LIBLCMS2)
+ void _setProfile(SVGICCColor *profile);
+ void _switchToProfile(gchar const *name);
+#endif
+ void _updateSliders(gint ignore);
+ void _profilesChanged(std::string const &name);
+
+ ColorICCSelector *_owner;
+ SelectedColor &_color;
+
+ gboolean _updating : 1;
+ gboolean _dragging : 1;
+
+ guint32 _fixupNeeded;
+ GtkWidget *_fixupBtn;
+ GtkWidget *_profileSel;
+
+ std::vector<ComponentUI> _compUI;
+
+ GtkAdjustment *_adj; // Channel adjustment
+ Inkscape::UI::Widget::ColorSlider *_slider;
+ GtkWidget *_sbtn; // Spinbutton
+ GtkWidget *_label; // Label
+
+#if defined(HAVE_LIBLCMS2)
+ std::string _profileName;
+ Inkscape::ColorProfile *_prof;
+ guint _profChannelCount;
+ gulong _profChangedID;
+#endif // defined(HAVE_LIBLCMS2)
+};
+
+
+
+const gchar *ColorICCSelector::MODE_NAME = N_("CMS");
+
+ColorICCSelector::ColorICCSelector(SelectedColor &color)
+ : _impl(nullptr)
+{
+ _impl = new ColorICCSelectorImpl(this, color);
+ init();
+ color.signal_changed.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+ // color.signal_dragged.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+}
+
+ColorICCSelector::~ColorICCSelector()
+{
+ if (_impl) {
+ delete _impl;
+ _impl = nullptr;
+ }
+}
+
+
+
+ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color)
+ : _owner(owner)
+ , _color(color)
+ , _updating(FALSE)
+ , _dragging(FALSE)
+ , _fixupNeeded(0)
+ , _fixupBtn(nullptr)
+ , _profileSel(nullptr)
+ , _compUI()
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _sbtn(nullptr)
+ , _label(nullptr)
+#if defined(HAVE_LIBLCMS2)
+ , _profileName()
+ , _prof(nullptr)
+ , _profChannelCount(0)
+ , _profChangedID(0)
+#endif // defined(HAVE_LIBLCMS2)
+{
+}
+
+ColorICCSelectorImpl::~ColorICCSelectorImpl()
+{
+ _adj = nullptr;
+ _sbtn = nullptr;
+ _label = nullptr;
+}
+
+void ColorICCSelector::init()
+{
+ gint row = 0;
+
+ _impl->_updating = FALSE;
+ _impl->_dragging = FALSE;
+
+ GtkWidget *t = GTK_WIDGET(gobj());
+
+ _impl->_compUI.clear();
+
+ // Create components
+ row = 0;
+
+
+ _impl->_fixupBtn = gtk_button_new_with_label(_("Fix"));
+ g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit),
+ (gpointer)_impl);
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+ gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value."));
+ gtk_widget_show(_impl->_fixupBtn);
+
+ attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1);
+
+ // Combobox and store with 2 columns : label (0) and full name (1)
+ GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ _impl->_profileSel = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, "text", 0, NULL);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1);
+
+ gtk_widget_show(_impl->_profileSel);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0);
+
+ attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1);
+
+#if defined(HAVE_LIBLCMS2)
+ _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed",
+ G_CALLBACK(ColorICCSelectorImpl::_profileSelected), (gpointer)_impl);
+#else
+ gtk_widget_set_sensitive(_impl->_profileSel, false);
+#endif // defined(HAVE_LIBLCMS2)
+
+
+ row++;
+
+// populate the data for colorspaces and channels:
+#if defined(HAVE_LIBLCMS2)
+ std::vector<colorspace::Component> things = colorspace::getColorSpaceInfo(cmsSigRgbData);
+#endif // defined(HAVE_LIBLCMS2)
+
+ for (size_t i = 0; i < maxColorspaceComponentCount; i++) {
+#if defined(HAVE_LIBLCMS2)
+ if (i < things.size()) {
+ _impl->_compUI.emplace_back(things[i]);
+ }
+ else {
+ _impl->_compUI.emplace_back();
+ }
+
+ std::string labelStr = (i < things.size()) ? things[i].name.c_str() : "";
+#else
+ _impl->_compUI.push_back(ComponentUI());
+
+ std::string labelStr = ".";
+#endif
+
+ _impl->_compUI[i]._label = gtk_label_new_with_mnemonic(labelStr.c_str());
+
+ gtk_widget_set_halign(_impl->_compUI[i]._label, GTK_ALIGN_END);
+ gtk_widget_show(_impl->_compUI[i]._label);
+ gtk_widget_set_no_show_all(_impl->_compUI[i]._label, TRUE);
+
+ attachToGridOrTable(t, _impl->_compUI[i]._label, 0, row, 1, 1);
+
+ // Adjustment
+ guint scaleValue = _impl->_compUI[i]._component.scale;
+ gdouble step = static_cast<gdouble>(scaleValue) / 100.0;
+ gdouble page = static_cast<gdouble>(scaleValue) / 10.0;
+ gint digits = (step > 0.9) ? 0 : 2;
+ _impl->_compUI[i]._adj = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, scaleValue, step, page, page));
+
+ // Slider
+ _impl->_compUI[i]._slider =
+ Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_impl->_compUI[i]._adj, true)));
+#if defined(HAVE_LIBLCMS2)
+ _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+#else
+ _impl->_compUI[i]._slider->set_tooltip_text(".");
+#endif // defined(HAVE_LIBLCMS2)
+ _impl->_compUI[i]._slider->show();
+ _impl->_compUI[i]._slider->set_no_show_all();
+
+ attachToGridOrTable(t, _impl->_compUI[i]._slider->gobj(), 1, row, 1, 1, true);
+
+ _impl->_compUI[i]._btn = gtk_spin_button_new(_impl->_compUI[i]._adj, step, digits);
+#if defined(HAVE_LIBLCMS2)
+ gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+#else
+ gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, ".");
+#endif // defined(HAVE_LIBLCMS2)
+ sp_dialog_defocus_on_enter(_impl->_compUI[i]._btn);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn);
+ gtk_widget_show(_impl->_compUI[i]._btn);
+ gtk_widget_set_no_show_all(_impl->_compUI[i]._btn, TRUE);
+
+ attachToGridOrTable(t, _impl->_compUI[i]._btn, 2, row, 1, 1, false, true);
+
+ _impl->_compUI[i]._map = g_new(guchar, 4 * 1024);
+ memset(_impl->_compUI[i]._map, 0x0ff, 1024 * 4);
+
+
+ // Signals
+ g_signal_connect(G_OBJECT(_impl->_compUI[i]._adj), "value_changed",
+ G_CALLBACK(ColorICCSelectorImpl::_adjustmentChanged), _impl);
+
+ _impl->_compUI[i]._slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed));
+ _impl->_compUI[i]._slider->signal_released.connect(
+ sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased));
+ _impl->_compUI[i]._slider->signal_value_changed.connect(
+ sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged));
+
+ row++;
+ }
+
+ // Label
+ _impl->_label = gtk_label_new_with_mnemonic(_("_A:"));
+
+ gtk_widget_set_halign(_impl->_label, GTK_ALIGN_END);
+ gtk_widget_show(_impl->_label);
+
+ attachToGridOrTable(t, _impl->_label, 0, row, 1, 1);
+
+ // Adjustment
+ _impl->_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 100.0, 1.0, 10.0, 10.0));
+
+ // Slider
+ _impl->_slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_impl->_adj, true)));
+ _impl->_slider->set_tooltip_text(_("Alpha (opacity)"));
+ _impl->_slider->show();
+
+ attachToGridOrTable(t, _impl->_slider->gobj(), 1, row, 1, 1, true);
+
+ _impl->_slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5),
+ SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0));
+
+
+ // Spinbutton
+ _impl->_sbtn = gtk_spin_button_new(GTK_ADJUSTMENT(_impl->_adj), 1.0, 0);
+ gtk_widget_set_tooltip_text(_impl->_sbtn, _("Alpha (opacity)"));
+ sp_dialog_defocus_on_enter(_impl->_sbtn);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_label), _impl->_sbtn);
+ gtk_widget_show(_impl->_sbtn);
+
+ attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true);
+
+ // Signals
+ g_signal_connect(G_OBJECT(_impl->_adj), "value_changed", G_CALLBACK(ColorICCSelectorImpl::_adjustmentChanged),
+ _impl);
+
+ _impl->_slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed));
+ _impl->_slider->signal_released.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased));
+ _impl->_slider->signal_value_changed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged));
+
+ gtk_widget_show(t);
+}
+
+void ColorICCSelectorImpl::_fixupHit(GtkWidget * /*src*/, gpointer data)
+{
+ ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data);
+ gtk_widget_set_sensitive(self->_fixupBtn, FALSE);
+ self->_adjustmentChanged(self->_compUI[0]._adj, self);
+}
+
+#if defined(HAVE_LIBLCMS2)
+void ColorICCSelectorImpl::_profileSelected(GtkWidget * /*src*/, gpointer data)
+{
+ ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data);
+
+ GtkTreeIter iter;
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(self->_profileSel), &iter)) {
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(self->_profileSel));
+ gchar *name = nullptr;
+
+ gtk_tree_model_get(store, &iter, 1, &name, -1);
+ self->_switchToProfile(name);
+ gtk_widget_set_tooltip_text(self->_profileSel, name);
+
+ g_free(name);
+ }
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+#if defined(HAVE_LIBLCMS2)
+void ColorICCSelectorImpl::_switchToProfile(gchar const *name)
+{
+ bool dirty = false;
+ SPColor tmp(_color.color());
+
+ if (name) {
+ if (tmp.icc && tmp.icc->colorProfile == name) {
+#ifdef DEBUG_LCMS
+ g_message("Already at name [%s]", name);
+#endif // DEBUG_LCMS
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("Need to switch to profile [%s]", name);
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ tmp.icc->colors.clear();
+ }
+ else {
+ tmp.icc = new SVGICCColor();
+ }
+ tmp.icc->colorProfile = name;
+ Inkscape::ColorProfile *newProf = SP_ACTIVE_DOCUMENT->getProfileManager()->find(name);
+ if (newProf) {
+ cmsHTRANSFORM trans = newProf->getTransfFromSRGB8();
+ if (trans) {
+ guint32 val = _color.color().toRGBA32(0);
+ guchar pre[4] = {
+ static_cast<guchar>(SP_RGBA32_R_U(val)),
+ static_cast<guchar>(SP_RGBA32_G_U(val)),
+ static_cast<guchar>(SP_RGBA32_B_U(val)),
+ 255};
+#ifdef DEBUG_LCMS
+ g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ cmsUInt16Number post[4] = { 0, 0, 0, 0 };
+ cmsDoTransform(trans, pre, post, 1);
+#ifdef DEBUG_LCMS
+ g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]);
+#endif // DEBUG_LCMS
+#if HAVE_LIBLCMS2
+ guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace()));
+#endif
+
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace()));
+
+ for (guint i = 0; i < count; i++) {
+ gdouble val =
+ (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1);
+#ifdef DEBUG_LCMS
+ g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val);
+#endif // DEBUG_LCMS
+ tmp.icc->colors.push_back(val);
+ }
+ cmsHTRANSFORM retrans = newProf->getTransfToSRGB8();
+ if (retrans) {
+ cmsDoTransform(retrans, post, pre, 1);
+#ifdef DEBUG_LCMS
+ g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff));
+ }
+
+ dirty = true;
+ }
+ }
+ }
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("NUKE THE ICC");
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ delete tmp.icc;
+ tmp.icc = nullptr;
+ dirty = true;
+ _fixupHit(nullptr, this);
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("No icc to nuke");
+#endif // DEBUG_LCMS
+ }
+ }
+
+ if (dirty) {
+#ifdef DEBUG_LCMS
+ g_message("+----------------");
+ g_message("+ new color is [%s]", tmp.toString().c_str());
+#endif // DEBUG_LCMS
+ _setProfile(tmp.icc);
+ //_adjustmentChanged( _compUI[0]._adj, SP_COLOR_ICC_SELECTOR(_csel) );
+ _color.setColor(tmp);
+#ifdef DEBUG_LCMS
+ g_message("+_________________");
+#endif // DEBUG_LCMS
+ }
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+#if defined(HAVE_LIBLCMS2)
+struct _cmp {
+ bool operator()(const SPObject * const & a, const SPObject * const & b)
+ {
+ const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a);
+ const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*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 <typename From, typename To>
+struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } };
+
+void ColorICCSelectorImpl::_profilesChanged(std::string const &name)
+{
+ GtkComboBox *combo = GTK_COMBO_BOX(_profileSel);
+
+ g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID);
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo));
+ gtk_list_store_clear(store);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1);
+
+ gtk_combo_box_set_active(combo, 0);
+
+ int index = 1;
+ std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile");
+
+ std::set<Inkscape::ColorProfile *, Inkscape::ColorProfile::pointerComparator> _current;
+ std::transform(current.begin(),
+ current.end(),
+ std::inserter(_current, _current.begin()),
+ static_caster<SPObject, Inkscape::ColorProfile>());
+
+ for (auto &it: _current) {
+ Inkscape::ColorProfile *prof = it;
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, ink_ellipsize_text(prof->name, 25).c_str(), 1, prof->name, -1);
+
+ if (name == prof->name) {
+ gtk_combo_box_set_active(combo, index);
+ gtk_widget_set_tooltip_text(_profileSel, prof->name);
+ }
+
+ index++;
+ }
+
+ g_signal_handler_unblock(G_OBJECT(_profileSel), _profChangedID);
+}
+#else
+void ColorICCSelectorImpl::_profilesChanged(std::string const & /*name*/) {}
+#endif // defined(HAVE_LIBLCMS2)
+
+void ColorICCSelector::on_show()
+{
+ Gtk::Grid::on_show();
+ _colorChanged();
+}
+
+// Helpers for setting color value
+
+void ColorICCSelector::_colorChanged()
+{
+ _impl->_updating = TRUE;
+// sp_color_icc_set_color( SP_COLOR_ICC( _icc ), &color );
+
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, _impl->_color.color().toRGBA32(_impl->_color.alpha()),
+ ((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+#ifdef DEBUG_LCMS
+ g_message("FLIPPIES!!!! %p '%s'", _impl->_color.color().icc,
+ (_impl->_color.color().icc ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+ _impl->_profilesChanged((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile : std::string(""));
+ ColorScales::setScaled(_impl->_adj, _impl->_color.alpha());
+
+#if defined(HAVE_LIBLCMS2)
+ _impl->_setProfile(_impl->_color.color().icc);
+ _impl->_fixupNeeded = 0;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+
+ if (_impl->_prof) {
+ if (_impl->_prof->getTransfToSRGB8()) {
+ cmsUInt16Number tmp[4];
+ for (guint i = 0; i < _impl->_profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_impl->_color.color().icc->colors.size() > i) {
+ if (_impl->_compUI[i]._component.scale == 256) {
+ val = (_impl->_color.color().icc->colors[i] + 128.0) /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ else {
+ val = _impl->_color.color().icc->colors[i] /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ }
+ tmp[i] = val * 0x0ffff;
+ }
+ guchar post[4] = { 0, 0, 0, 0 };
+ cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, tmp, post, 1);
+ guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255);
+ if (other != _impl->_color.color().toRGBA32(255)) {
+ _impl->_fixupNeeded = other;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE);
+#ifdef DEBUG_LCMS
+ g_message("Color needs to change 0x%06x to 0x%06x", _color.toRGBA32(255) >> 8, other >> 8);
+#endif // DEBUG_LCMS
+ }
+ }
+ }
+ }
+#else
+//(void)color;
+#endif // defined(HAVE_LIBLCMS2)
+ _impl->_updateSliders(-1);
+
+
+ _impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_colorChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+#if defined(HAVE_LIBLCMS2)
+void ColorICCSelectorImpl::_setProfile(SVGICCColor *profile)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, ((profile) ? profile->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+ bool profChanged = false;
+ if (_prof && (!profile || (_profileName != profile->colorProfile))) {
+ // Need to clear out the prior one
+ profChanged = true;
+ _profileName.clear();
+ _prof = nullptr;
+ _profChannelCount = 0;
+ }
+ else if (profile && !_prof) {
+ profChanged = true;
+ }
+
+ for (auto & i : _compUI) {
+ gtk_widget_hide(i._label);
+ i._slider->hide();
+ gtk_widget_hide(i._btn);
+ }
+
+ if (profile) {
+ _prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(profile->colorProfile.c_str());
+ if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) {
+#if HAVE_LIBLCMS2
+ _profChannelCount = cmsChannelsOf(asICColorSpaceSig(_prof->getColorSpace()));
+#endif
+
+ if (profChanged) {
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace()));
+ for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) {
+ _compUI[i]._component = things[i];
+ }
+
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label),
+ (i < things.size()) ? things[i].name.c_str() : "");
+
+ _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+ gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+
+ _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff),
+ SPColor(0.5, 0.5, 0.5).toRGBA32(0xff),
+ SPColor(1.0, 1.0, 1.0).toRGBA32(0xff));
+ /*
+ _compUI[i]._adj = GTK_ADJUSTMENT( gtk_adjustment_new( val, 0.0, _fooScales[i],
+ step, page, page ) );
+ g_signal_connect( G_OBJECT( _compUI[i]._adj ), "value_changed", G_CALLBACK(
+ _adjustmentChanged ), _csel );
+
+ sp_color_slider_set_adjustment( SP_COLOR_SLIDER(_compUI[i]._slider),
+ _compUI[i]._adj );
+ gtk_spin_button_set_adjustment( GTK_SPIN_BUTTON(_compUI[i]._btn),
+ _compUI[i]._adj );
+ gtk_spin_button_set_digits( GTK_SPIN_BUTTON(_compUI[i]._btn), digits );
+ */
+ gtk_widget_show(_compUI[i]._label);
+ _compUI[i]._slider->show();
+ gtk_widget_show(_compUI[i]._btn);
+ // gtk_adjustment_set_value( _compUI[i]._adj, 0.0 );
+ // gtk_adjustment_set_value( _compUI[i]._adj, val );
+ }
+ for (size_t i = _profChannelCount; i < _compUI.size(); i++) {
+ gtk_widget_hide(_compUI[i]._label);
+ _compUI[i]._slider->hide();
+ gtk_widget_hide(_compUI[i]._btn);
+ }
+ }
+ }
+ else {
+ // Give up for now on named colors
+ _prof = nullptr;
+ }
+ }
+
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_setProfile()", this);
+#endif // DEBUG_LCMS
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+void ColorICCSelectorImpl::_updateSliders(gint ignore)
+{
+#if defined(HAVE_LIBLCMS2)
+ if (_color.color().icc) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_color.color().icc->colors.size() > i) {
+ if (_compUI[i]._component.scale == 256) {
+ val = (_color.color().icc->colors[i] + 128.0) / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ else {
+ val = _color.color().icc->colors[i] / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ }
+ gtk_adjustment_set_value(_compUI[i]._adj, val);
+ }
+
+ if (_prof) {
+ if (_prof->getTransfToSRGB8()) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ if (static_cast<gint>(i) != ignore) {
+ cmsUInt16Number *scratch = getScratch();
+ cmsUInt16Number filler[4] = { 0, 0, 0, 0 };
+ for (guint j = 0; j < _profChannelCount; j++) {
+ filler[j] = 0x0ffff * ColorScales::getScaled(_compUI[j]._adj);
+ }
+
+ cmsUInt16Number *p = scratch;
+ for (guint x = 0; x < 1024; x++) {
+ for (guint j = 0; j < _profChannelCount; j++) {
+ if (j == i) {
+ *p++ = x * 0x0ffff / 1024;
+ }
+ else {
+ *p++ = filler[j];
+ }
+ }
+ }
+
+ cmsHTRANSFORM trans = _prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, scratch, _compUI[i]._map, 1024);
+ if (_compUI[i]._slider)
+ {
+ _compUI[i]._slider->setMap(_compUI[i]._map);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+#else
+ (void)ignore;
+#endif // defined(HAVE_LIBLCMS2)
+
+ guint32 start = _color.color().toRGBA32(0x00);
+ guint32 mid = _color.color().toRGBA32(0x7f);
+ guint32 end = _color.color().toRGBA32(0xff);
+
+ _slider->setColors(start, mid, end);
+}
+
+
+void ColorICCSelectorImpl::_adjustmentChanged(GtkAdjustment *adjustment, ColorICCSelectorImpl *cs)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_adjustmentChanged()", cs);
+#endif // DEBUG_LCMS
+
+ ColorICCSelector *iccSelector = cs->_owner;
+ if (iccSelector->_impl->_updating) {
+ return;
+ }
+
+ iccSelector->_impl->_updating = TRUE;
+
+ gint match = -1;
+
+ SPColor newColor(iccSelector->_impl->_color.color());
+ gfloat scaled = ColorScales::getScaled(iccSelector->_impl->_adj);
+ if (iccSelector->_impl->_adj == adjustment) {
+#ifdef DEBUG_LCMS
+ g_message("ALPHA");
+#endif // DEBUG_LCMS
+ }
+ else {
+#if defined(HAVE_LIBLCMS2)
+ for (size_t i = 0; i < iccSelector->_impl->_compUI.size(); i++) {
+ if (iccSelector->_impl->_compUI[i]._adj == adjustment) {
+ match = i;
+ break;
+ }
+ }
+ if (match >= 0) {
+#ifdef DEBUG_LCMS
+ g_message(" channel %d", match);
+#endif // DEBUG_LCMS
+ }
+
+
+ cmsUInt16Number tmp[4];
+ for (guint i = 0; i < 4; i++) {
+ tmp[i] = ColorScales::getScaled(iccSelector->_impl->_compUI[i]._adj) * 0x0ffff;
+ }
+ guchar post[4] = { 0, 0, 0, 0 };
+
+ cmsHTRANSFORM trans = iccSelector->_impl->_prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, tmp, post, 1);
+ }
+
+ SPColor other(SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255));
+ other.icc = new SVGICCColor();
+ if (iccSelector->_impl->_color.color().icc) {
+ other.icc->colorProfile = iccSelector->_impl->_color.color().icc->colorProfile;
+ }
+
+ guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255);
+ guint32 newer = other.toRGBA32(255);
+
+ if (prior != newer) {
+#ifdef DEBUG_LCMS
+ g_message("Transformed color from 0x%08x to 0x%08x", prior, newer);
+ g_message(" ~~~~ FLIP");
+#endif // DEBUG_LCMS
+ newColor = other;
+ newColor.icc->colors.clear();
+ for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) {
+ gdouble val = ColorScales::getScaled(iccSelector->_impl->_compUI[i]._adj);
+ val *= iccSelector->_impl->_compUI[i]._component.scale;
+ if (iccSelector->_impl->_compUI[i]._component.scale == 256) {
+ val -= 128;
+ }
+ newColor.icc->colors.push_back(val);
+ }
+ }
+#endif // defined(HAVE_LIBLCMS2)
+ }
+ iccSelector->_impl->_color.setColorAlpha(newColor, scaled);
+ // iccSelector->_updateInternals( newColor, scaled, iccSelector->_impl->_dragging );
+ iccSelector->_impl->_updateSliders(match);
+
+ iccSelector->_impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_adjustmentChanged()", cs);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_sliderGrabbed()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (!iccSelector->_dragging) {
+ // iccSelector->_dragging = TRUE;
+ // iccSelector->_grabbed();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_impl->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+void ColorICCSelectorImpl::_sliderReleased()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (iccSelector->_dragging) {
+ // iccSelector->_dragging = FALSE;
+ // iccSelector->_released();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+#ifdef DEBUG_LCMS
+void ColorICCSelectorImpl::_sliderChanged(SPColorSlider *slider, SPColorICCSelector *cs)
+#else
+void ColorICCSelectorImpl::_sliderChanged()
+#endif // DEBUG_LCMS
+{
+#ifdef DEBUG_LCMS
+ g_message("Changed %p and %p", slider, cs);
+#endif // DEBUG_LCMS
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+}
+
+Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color));
+ return w;
+}
+
+Glib::ustring ColorICCSelectorFactory::modeName() const { return gettext(ColorICCSelector::MODE_NAME); }
+}
+}
+}
+/*
+ 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/widget/color-icc-selector.h b/src/ui/widget/color-icc-selector.h
new file mode 100644
index 0000000..2c5ec41
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.h
@@ -0,0 +1,75 @@
+// 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.
+ */
+#ifndef SEEN_SP_COLOR_ICC_SELECTOR_H
+#define SEEN_SP_COLOR_ICC_SELECTOR_H
+
+#include <gtkmm/widget.h>
+#include <gtkmm/grid.h>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+
+class ColorProfile;
+
+namespace UI {
+namespace Widget {
+
+class ColorICCSelectorImpl;
+
+class ColorICCSelector
+ : public Gtk::Grid
+ {
+ public:
+ static const gchar *MODE_NAME;
+
+ ColorICCSelector(SelectedColor &color);
+ ~ColorICCSelector() override;
+
+ virtual void init();
+
+ protected:
+ void on_show() override;
+
+ virtual void _colorChanged();
+
+ void _recalcColor(gboolean changing);
+
+ private:
+ friend class ColorICCSelectorImpl;
+
+ // By default, disallow copy constructor and assignment operator
+ ColorICCSelector(const ColorICCSelector &obj);
+ ColorICCSelector &operator=(const ColorICCSelector &obj);
+
+ ColorICCSelectorImpl *_impl;
+};
+
+
+class ColorICCSelectorFactory : public ColorSelectorFactory {
+ public:
+ Gtk::Widget *createWidget(SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+};
+}
+}
+}
+#endif // SEEN_SP_COLOR_ICC_SELECTOR_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/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp
new file mode 100644
index 0000000..474b4d2
--- /dev/null
+++ b/src/ui/widget/color-notebook.cpp
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages
+ *//*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2001-2014 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
+
+#undef SPCS_PREVIEW
+#define noDUMP_CHANGE_INFO
+
+#include <glibmm/i18n.h>
+#include <gtkmm/label.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "cms-system.h"
+#include "document.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "profile-manager.h"
+
+#include "object/color-profile.h"
+#include "ui/icon-loader.h"
+
+#include "svg/svg-icc-color.h"
+
+#include "ui/dialog-events.h"
+#include "ui/tools-switch.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/color-entry.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-wheel-selector.h"
+
+#include "widgets/spw-utilities.h"
+
+using Inkscape::CMSSystem;
+
+#define XPAD 4
+#define YPAD 1
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+ColorNotebook::ColorNotebook(SelectedColor &color)
+ : Gtk::Grid()
+ , _selected_color(color)
+{
+ set_name("ColorNotebook");
+
+ Page *page;
+
+ page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_RGB), true);
+ _available_pages.push_back(page);
+ page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_HSL), true);
+ _available_pages.push_back(page);
+ page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_HSV), true);
+ _available_pages.push_back(page);
+ page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_CMYK), true);
+ _available_pages.push_back(page);
+ page = new Page(new ColorWheelSelectorFactory, true);
+ _available_pages.push_back(page);
+#if defined(HAVE_LIBLCMS2)
+ page = new Page(new ColorICCSelectorFactory, true);
+ _available_pages.push_back(page);
+#endif
+
+ _initUI();
+
+ _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+}
+
+ColorNotebook::~ColorNotebook()
+{
+ if (_buttons) {
+ delete[] _buttons;
+ _buttons = nullptr;
+ }
+}
+
+ColorNotebook::Page::Page(Inkscape::UI::ColorSelectorFactory *selector_factory, bool enabled_full)
+ : selector_factory(selector_factory)
+ , enabled_full(enabled_full)
+{
+}
+
+
+void ColorNotebook::_initUI()
+{
+ guint row = 0;
+
+ Gtk::Notebook *notebook = Gtk::manage(new Gtk::Notebook);
+ notebook->show();
+ notebook->set_show_border(false);
+ notebook->set_show_tabs(false);
+ _book = GTK_WIDGET(notebook->gobj());
+
+ _buttonbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
+ gtk_box_set_homogeneous(GTK_BOX(_buttonbox), TRUE);
+
+ gtk_widget_show(_buttonbox);
+ _buttons = new GtkWidget *[_available_pages.size()];
+
+ for (int i = 0; static_cast<size_t>(i) < _available_pages.size(); i++) {
+ _addPage(_available_pages[i]);
+ }
+
+ gtk_widget_set_margin_start(_buttonbox, XPAD);
+ gtk_widget_set_margin_end(_buttonbox, XPAD);
+ gtk_widget_set_margin_top(_buttonbox, YPAD);
+ gtk_widget_set_margin_bottom(_buttonbox, YPAD);
+ gtk_widget_set_hexpand(_buttonbox, TRUE);
+ gtk_widget_set_valign(_buttonbox, GTK_ALIGN_CENTER);
+ attach(*Glib::wrap(_buttonbox), 0, row, 2, 1);
+
+ row++;
+
+ gtk_widget_set_margin_start(_book, XPAD * 2);
+ gtk_widget_set_margin_end(_book, XPAD * 2);
+ gtk_widget_set_margin_top(_book, YPAD);
+ gtk_widget_set_margin_bottom(_book, YPAD);
+ gtk_widget_set_hexpand(_book, TRUE);
+ gtk_widget_set_vexpand(_book, TRUE);
+ attach(*notebook, 0, row, 2, 1);
+
+ // restore the last active page
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _setCurrentPage(prefs->getInt("/colorselector/page", 0));
+ row++;
+
+ GtkWidget *rgbabox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+#if defined(HAVE_LIBLCMS2)
+ /* Create color management icons */
+ _box_colormanaged = gtk_event_box_new();
+ GtkWidget *colormanaged = sp_get_icon_image("color-management", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_colormanaged), colormanaged);
+ gtk_widget_set_tooltip_text(_box_colormanaged, _("Color Managed"));
+ gtk_widget_set_sensitive(_box_colormanaged, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_colormanaged, FALSE, FALSE, 2);
+
+ _box_outofgamut = gtk_event_box_new();
+ GtkWidget *outofgamut = sp_get_icon_image("out-of-gamut-icon", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_outofgamut), outofgamut);
+ gtk_widget_set_tooltip_text(_box_outofgamut, _("Out of gamut!"));
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_outofgamut, FALSE, FALSE, 2);
+
+ _box_toomuchink = gtk_event_box_new();
+ GtkWidget *toomuchink = sp_get_icon_image("too-much-ink-icon", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_toomuchink), toomuchink);
+ gtk_widget_set_tooltip_text(_box_toomuchink, _("Too much ink!"));
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_toomuchink, FALSE, FALSE, 2);
+#endif // defined(HAVE_LIBLCMS2)
+
+
+ /* Color picker */
+ GtkWidget *picker = sp_get_icon_image("color-picker", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ _btn_picker = gtk_button_new();
+ gtk_button_set_relief(GTK_BUTTON(_btn_picker), GTK_RELIEF_NONE);
+ gtk_container_add(GTK_CONTAINER(_btn_picker), picker);
+ gtk_widget_set_tooltip_text(_btn_picker, _("Pick colors from image"));
+ gtk_box_pack_start(GTK_BOX(rgbabox), _btn_picker, FALSE, FALSE, 2);
+ g_signal_connect(G_OBJECT(_btn_picker), "clicked", G_CALLBACK(ColorNotebook::_onPickerClicked), this);
+
+ /* Create RGBA entry and color preview */
+ _rgbal = gtk_label_new_with_mnemonic(_("RGBA_:"));
+ gtk_widget_set_halign(_rgbal, GTK_ALIGN_END);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _rgbal, TRUE, TRUE, 2);
+
+ ColorEntry *rgba_entry = Gtk::manage(new ColorEntry(_selected_color));
+ sp_dialog_defocus_on_enter(GTK_WIDGET(rgba_entry->gobj()));
+ gtk_box_pack_start(GTK_BOX(rgbabox), GTK_WIDGET(rgba_entry->gobj()), FALSE, FALSE, 0);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_rgbal), GTK_WIDGET(rgba_entry->gobj()));
+
+ gtk_widget_show_all(rgbabox);
+
+#if defined(HAVE_LIBLCMS2)
+ // the "too much ink" icon is initially hidden
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+#endif // defined(HAVE_LIBLCMS2)
+
+ gtk_widget_set_margin_start(rgbabox, XPAD);
+ gtk_widget_set_margin_end(rgbabox, XPAD);
+ gtk_widget_set_margin_top(rgbabox, YPAD);
+ gtk_widget_set_margin_bottom(rgbabox, YPAD);
+ attach(*Glib::wrap(rgbabox), 0, row, 2, 1);
+
+#ifdef SPCS_PREVIEW
+ _p = sp_color_preview_new(0xffffffff);
+ gtk_widget_show(_p);
+ attach(*Glib::wrap(_p), 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, XPAD, YPAD);
+#endif
+
+ g_signal_connect(G_OBJECT(_book), "switch-page", G_CALLBACK(ColorNotebook::_onPageSwitched), this);
+}
+
+void ColorNotebook::_onPickerClicked(GtkWidget * /*widget*/, ColorNotebook * /*colorbook*/)
+{
+ // Set the dropper into a "one click" mode, so it reverts to the previous tool after a click
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/dropper/onetimepick", true);
+ Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP);
+}
+
+void ColorNotebook::_onButtonClicked(GtkWidget *widget, ColorNotebook *nb)
+{
+ if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
+ return;
+ }
+
+ for (gint i = 0; i < gtk_notebook_get_n_pages(GTK_NOTEBOOK(nb->_book)); i++) {
+ if (nb->_buttons[i] == widget) {
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(nb->_book), i);
+ }
+ }
+}
+
+void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); }
+
+void ColorNotebook::_onPageSwitched(GtkNotebook *notebook, GtkWidget *page, guint page_num, ColorNotebook *colorbook)
+{
+ if (colorbook->get_visible()) {
+ // remember the page we switched to
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/colorselector/page", page_num);
+ }
+}
+
+
+// TODO pass in param so as to avoid the need for SP_ACTIVE_DOCUMENT
+void ColorNotebook::_updateICCButtons()
+{
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+
+ g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0));
+
+#if defined(HAVE_LIBLCMS2)
+ /* update color management icon*/
+ gtk_widget_set_sensitive(_box_colormanaged, color.icc != nullptr);
+
+ /* update out-of-gamut icon */
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *target_profile =
+ SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (target_profile)
+ gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color));
+ }
+
+ /* update too-much-ink icon */
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (prof && CMSSystem::isPrintColorSpace(prof)) {
+ gtk_widget_show(GTK_WIDGET(_box_toomuchink));
+ double ink_sum = 0;
+ for (double i : color.icc->colors) {
+ ink_sum += i;
+ }
+
+ /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured
+ color,
+ which means the paper can get too wet due to an excessive amount of ink. This may lead to several
+ issues
+ such as misalignment and poor quality of printing in general.*/
+ if (ink_sum > 3.2)
+ gtk_widget_set_sensitive(_box_toomuchink, true);
+ }
+ else {
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+ }
+ }
+#endif // defined(HAVE_LIBLCMS2)
+}
+
+void ColorNotebook::_setCurrentPage(int i)
+{
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(_book), i);
+
+ if (_buttons && (static_cast<size_t>(i) < _available_pages.size())) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_buttons[i]), TRUE);
+ }
+}
+
+void ColorNotebook::_addPage(Page &page)
+{
+ Gtk::Widget *selector_widget;
+
+ selector_widget = page.selector_factory->createWidget(_selected_color);
+ if (selector_widget) {
+ selector_widget->show();
+
+ Glib::ustring mode_name = page.selector_factory->modeName();
+ Gtk::Widget *tab_label = Gtk::manage(new Gtk::Label(mode_name));
+ tab_label->set_name("ColorModeLabel");
+ gint page_num = gtk_notebook_append_page(GTK_NOTEBOOK(_book), selector_widget->gobj(), tab_label->gobj());
+
+ _buttons[page_num] = gtk_radio_button_new_with_label(nullptr, mode_name.c_str());
+ gtk_widget_set_name(_buttons[page_num], "ColorModeButton");
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(_buttons[page_num]), FALSE);
+ if (page_num > 0) {
+ auto g = Glib::wrap(GTK_RADIO_BUTTON(_buttons[0]))->get_group();
+ Glib::wrap(GTK_RADIO_BUTTON(_buttons[page_num]))->set_group(g);
+ }
+ gtk_widget_show(_buttons[page_num]);
+ gtk_box_pack_start(GTK_BOX(_buttonbox), _buttons[page_num], TRUE, TRUE, 0);
+
+ g_signal_connect(G_OBJECT(_buttons[page_num]), "clicked", G_CALLBACK(_onButtonClicked), this);
+ }
+}
+}
+}
+}
+
+/*
+ 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/widget/color-notebook.h b/src/ui/widget/color-notebook.h
new file mode 100644
index 0000000..c7bc7b5
--- /dev/null
+++ b/src/ui/widget/color-notebook.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages
+ *//*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2001-2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_NOTEBOOK_H
+#define SEEN_SP_COLOR_NOTEBOOK_H
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <boost/ptr_container/ptr_vector.hpp>
+#include <gtkmm/grid.h>
+#include <glib.h>
+
+#include "color.h"
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorNotebook
+ : public Gtk::Grid
+{
+public:
+ ColorNotebook(SelectedColor &color);
+ ~ColorNotebook() override;
+
+protected:
+ struct Page {
+ Page(Inkscape::UI::ColorSelectorFactory *selector_factory, bool enabled_full);
+
+ Inkscape::UI::ColorSelectorFactory *selector_factory;
+ bool enabled_full;
+ };
+
+ virtual void _initUI();
+ void _addPage(Page &page);
+
+ static void _onButtonClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ static void _onPageSwitched(GtkNotebook *notebook, GtkWidget *page, guint page_num, ColorNotebook *colorbook);
+ virtual void _onSelectedColorChanged();
+
+ void _updateICCButtons();
+ void _setCurrentPage(int i);
+
+ Inkscape::UI::SelectedColor &_selected_color;
+ gulong _entryId;
+ GtkWidget *_book;
+ GtkWidget *_buttonbox;
+ GtkWidget **_buttons;
+ GtkWidget *_rgbal; /* RGBA entry */
+#if defined(HAVE_LIBLCMS2)
+ GtkWidget *_box_outofgamut, *_box_colormanaged, *_box_toomuchink;
+#endif // defined(HAVE_LIBLCMS2)
+ GtkWidget *_btn_picker;
+ GtkWidget *_p; /* Color preview */
+ boost::ptr_vector<Page> _available_pages;
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorNotebook(const ColorNotebook &obj) = delete;
+ ColorNotebook &operator=(const ColorNotebook &obj) = delete;
+};
+}
+}
+}
+#endif // SEEN_SP_COLOR_NOTEBOOK_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/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp
new file mode 100644
index 0000000..5beb090
--- /dev/null
+++ b/src/ui/widget/color-picker.cpp
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Abhishek Sharma
+ *
+ * Copyright (C) Authors 2000-2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "color-picker.h"
+#include "inkscape.h"
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "ui/dialog-events.h"
+
+#include "ui/widget/color-notebook.h"
+#include "verbs.h"
+
+
+static bool _in_use = false;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPicker::ColorPicker (const Glib::ustring& title, const Glib::ustring& tip,
+ guint32 rgba, bool undo)
+ : _preview(rgba), _title(title), _rgba(rgba), _undo(undo),
+ _colorSelectorDialog("dialogs.colorpickerwindow")
+{
+ setupDialog(title);
+ _preview.show();
+ add (_preview);
+ set_tooltip_text (tip);
+ _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+ _selected_color.signal_released.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+}
+
+ColorPicker::~ColorPicker()
+{
+ closeWindow();
+}
+
+void ColorPicker::setupDialog(const Glib::ustring &title)
+{
+ GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj());
+ sp_transientize(dlg);
+
+ _colorSelectorDialog.hide();
+ _colorSelectorDialog.set_title (title);
+ _colorSelectorDialog.set_border_width (4);
+
+ _color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ _colorSelectorDialog.get_content_area()->pack_start (
+ *_color_selector, true, true, 0);
+ _color_selector->show();
+}
+
+void ColorPicker::setSensitive(bool sensitive) { set_sensitive(sensitive); }
+
+void ColorPicker::setRgba32 (guint32 rgba)
+{
+ if (_in_use) return;
+
+ _preview.setRgba32 (rgba);
+ _rgba = rgba;
+ if (_color_selector)
+ {
+ _updating = true;
+ _selected_color.setValue(rgba);
+ _updating = false;
+ }
+}
+
+void ColorPicker::closeWindow()
+{
+ _colorSelectorDialog.hide();
+}
+
+void ColorPicker::on_clicked()
+{
+ if (_color_selector)
+ {
+ _updating = true;
+ _selected_color.setValue(_rgba);
+ _updating = false;
+ }
+ _colorSelectorDialog.show();
+ Glib::RefPtr<Gdk::Window> window = _colorSelectorDialog.get_parent_window();
+ if (window) {
+ window->focus(1);
+ }
+}
+
+void ColorPicker::on_changed (guint32)
+{
+}
+
+void ColorPicker::_onSelectedColorChanged() {
+ if (_updating) {
+ return;
+ }
+
+ if (_in_use) {
+ return;
+ } else {
+ _in_use = true;
+ }
+
+ guint32 rgba = _selected_color.value();
+ _preview.setRgba32(rgba);
+
+ if (_undo && SP_ACTIVE_DESKTOP) {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_NONE,
+ /* TODO: annotate */ "color-picker.cpp:130");
+ }
+
+ on_changed(rgba);
+ _in_use = false;
+ _changed_signal.emit(rgba);
+ _rgba = rgba;
+}
+
+}//namespace Widget
+}//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/widget/color-picker.h b/src/ui/widget/color-picker.h
new file mode 100644
index 0000000..b98f832
--- /dev/null
+++ b/src/ui/widget/color-picker.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color picker button and window.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) Authors 2000-2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __COLOR_PICKER_H__
+#define __COLOR_PICKER_H__
+
+#include "labelled.h"
+
+#include <cstddef>
+
+#include "ui/selected-color.h"
+#include "ui/widget/color-preview.h"
+#include <gtkmm/button.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/window.h>
+#include <sigc++/sigc++.h>
+
+struct SPColorSelector;
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Widget
+{
+
+
+class ColorPicker : public Gtk::Button {
+public:
+
+ ColorPicker (const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const guint32 rgba,
+ bool undo);
+
+ ~ColorPicker() override;
+
+ void setRgba32 (guint32 rgba);
+ void setSensitive(bool sensitive);
+ void closeWindow();
+ sigc::connection connectChanged (const sigc::slot<void,guint>& slot)
+ { return _changed_signal.connect (slot); }
+
+protected:
+
+ void _onSelectedColorChanged();
+ void on_clicked() override;
+ virtual void on_changed (guint32);
+
+ ColorPreview _preview;
+
+ /*const*/ Glib::ustring _title;
+ sigc::signal<void,guint32> _changed_signal;
+ guint32 _rgba;
+ bool _undo;
+ bool _updating;
+
+ //Dialog
+ void setupDialog(const Glib::ustring &title);
+ //Inkscape::UI::Dialog::Dialog _colorSelectorDialog;
+ Gtk::Dialog _colorSelectorDialog;
+ SelectedColor _selected_color;
+ Gtk::Widget *_color_selector;
+};
+
+
+class LabelledColorPicker : public Labelled {
+public:
+
+ LabelledColorPicker (const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const guint32 rgba,
+ bool undo) : Labelled(label, tip, new ColorPicker(title, tip, rgba, undo)) {}
+
+ ~LabelledColorPicker() override
+ { static_cast<ColorPicker*>(_widget)->~ColorPicker(); }
+
+ void setRgba32 (guint32 rgba)
+ { static_cast<ColorPicker*>(_widget)->setRgba32 (rgba); }
+
+ void closeWindow()
+ { static_cast<ColorPicker*>(_widget)->closeWindow (); }
+
+ sigc::connection connectChanged (const sigc::slot<void,guint>& slot)
+ { return static_cast<ColorPicker*>(_widget)->connectChanged(slot); }
+};
+
+}//namespace Widget
+}//namespace UI
+}//namespace Inkscape
+
+#endif /* !__COLOR_PICKER_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/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp
new file mode 100644
index 0000000..ac8fc57
--- /dev/null
+++ b/src/ui/widget/color-preview.cpp
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2005 Authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/color-preview.h"
+#include "display/cairo-utils.h"
+#include <cairo.h>
+
+#define SPCP_DEFAULT_WIDTH 32
+#define SPCP_DEFAULT_HEIGHT 12
+
+namespace Inkscape {
+ namespace UI {
+ namespace Widget {
+
+ColorPreview::ColorPreview (guint32 rgba)
+{
+ _rgba = rgba;
+ set_has_window(false);
+ set_name("ColorPreview");
+}
+
+void
+ColorPreview::on_size_allocate (Gtk::Allocation &all)
+{
+ set_allocation (all);
+ if (get_is_drawable())
+ queue_draw();
+}
+
+void
+ColorPreview::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const
+{
+ minimum_height = natural_height = SPCP_DEFAULT_HEIGHT;
+}
+
+void
+ColorPreview::get_preferred_height_for_width_vfunc(int /* width */, int& minimum_height, int& natural_height) const
+{
+ minimum_height = natural_height = SPCP_DEFAULT_HEIGHT;
+}
+
+void
+ColorPreview::get_preferred_width_vfunc(int& minimum_width, int& natural_width) const
+{
+ minimum_width = natural_width = SPCP_DEFAULT_WIDTH;
+}
+
+void
+ColorPreview::get_preferred_width_for_height_vfunc(int /* height */, int& minimum_width, int& natural_width) const
+{
+ minimum_width = natural_width = SPCP_DEFAULT_WIDTH;
+}
+
+void
+ColorPreview::setRgba32 (guint32 rgba)
+{
+ _rgba = rgba;
+
+ if (get_is_drawable())
+ queue_draw();
+}
+
+bool
+ColorPreview::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ double x, y, width, height;
+ const Gtk::Allocation& allocation = get_allocation();
+ x = 0;
+ y = 0;
+ width = allocation.get_width()/2.0;
+ height = allocation.get_height();
+
+ double radius = height / 7.5;
+ double degrees = M_PI / 180.0;
+ cairo_new_sub_path (cr->cobj());
+ cairo_line_to(cr->cobj(), width, 0);
+ cairo_line_to(cr->cobj(), width, height);
+ cairo_arc (cr->cobj(), x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
+ cairo_arc (cr->cobj(), x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
+ cairo_close_path (cr->cobj());
+
+ /* Transparent area */
+
+ cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard();
+
+ cairo_set_source(cr->cobj(), checkers);
+ cr->fill_preserve();
+ ink_cairo_set_source_rgba32(cr->cobj(), _rgba);
+ cr->fill();
+ cairo_pattern_destroy(checkers);
+
+ /* Solid area */
+
+ x = width;
+
+ cairo_new_sub_path (cr->cobj());
+ cairo_arc (cr->cobj(), x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
+ cairo_arc (cr->cobj(), x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
+ cairo_line_to(cr->cobj(), x, height);
+ cairo_line_to(cr->cobj(), x, y);
+ cairo_close_path (cr->cobj());
+ ink_cairo_set_source_rgba32(cr->cobj(), _rgba | 0xff);
+ cr->fill();
+
+ return true;
+}
+
+GdkPixbuf*
+ColorPreview::toPixbuf (int width, int height)
+{
+ GdkRectangle carea;
+ gint w2;
+ w2 = width / 2;
+
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ /* Transparent area */
+ carea.x = 0;
+ carea.y = 0;
+ carea.width = w2;
+ carea.height = height;
+
+ cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard();
+
+ cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height);
+ cairo_set_source(ct, checkers);
+ cairo_fill_preserve(ct);
+ ink_cairo_set_source_rgba32(ct, _rgba);
+ cairo_fill(ct);
+
+ cairo_pattern_destroy(checkers);
+
+ /* Solid area */
+ carea.x = w2;
+ carea.y = 0;
+ carea.width = width - w2;
+ carea.height = height;
+
+ cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height);
+ ink_cairo_set_source_rgba32(ct, _rgba | 0xff);
+ cairo_fill(ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+}}}
+
+/*
+ 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/widget/color-preview.h b/src/ui/widget/color-preview.h
new file mode 100644
index 0000000..b789579
--- /dev/null
+++ b/src/ui/widget/color-preview.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_COLOR_PREVIEW_H
+#define SEEN_COLOR_PREVIEW_H
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2005 Authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A simple color preview widget, mainly used within a picker button.
+ */
+class ColorPreview : public Gtk::Widget {
+public:
+ ColorPreview (guint32 rgba);
+ void setRgba32 (guint32 rgba);
+ GdkPixbuf* toPixbuf (int width, int height);
+
+protected:
+ void on_size_allocate (Gtk::Allocation &all) override;
+
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+ void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override;
+ void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override;
+ void get_preferred_width_for_height_vfunc(int height, int& minimum_width, int& natural_width) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+
+ guint32 _rgba;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_COLOR_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 :
diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp
new file mode 100644
index 0000000..a93ab2a
--- /dev/null
+++ b/src/ui/widget/color-scales.cpp
@@ -0,0 +1,736 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors:
+ * see git history
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/adjustment.h>
+#include <glibmm/i18n.h>
+
+#include "ui/dialog-events.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+
+#define CSC_CHANNEL_R (1 << 0)
+#define CSC_CHANNEL_G (1 << 1)
+#define CSC_CHANNEL_B (1 << 2)
+#define CSC_CHANNEL_A (1 << 3)
+#define CSC_CHANNEL_H (1 << 0)
+#define CSC_CHANNEL_S (1 << 1)
+#define CSC_CHANNEL_V (1 << 2)
+#define CSC_CHANNEL_C (1 << 0)
+#define CSC_CHANNEL_M (1 << 1)
+#define CSC_CHANNEL_Y (1 << 2)
+#define CSC_CHANNEL_K (1 << 3)
+#define CSC_CHANNEL_CMYKA (1 << 4)
+
+#define CSC_CHANNELS_ALL 0
+
+#define XPAD 4
+#define YPAD 1
+
+#define noDUMP_CHANGE_INFO 1
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+static const gchar *sp_color_scales_hue_map();
+
+const gchar *ColorScales::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV") };
+
+ColorScales::ColorScales(SelectedColor &color, SPColorScalesMode mode)
+ : Gtk::Grid()
+ , _color(color)
+ , _rangeLimit(255.0)
+ , _updating(FALSE)
+ , _dragging(FALSE)
+ , _mode(SP_COLOR_SCALES_MODE_NONE)
+{
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _a[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+
+ _initUI(mode);
+
+ _color.signal_changed.connect(sigc::mem_fun(this, &ColorScales::_onColorChanged));
+ _color.signal_dragged.connect(sigc::mem_fun(this, &ColorScales::_onColorChanged));
+}
+
+ColorScales::~ColorScales()
+{
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _a[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+}
+
+void ColorScales::_initUI(SPColorScalesMode mode)
+{
+ gint i;
+
+ _updating = FALSE;
+ _dragging = FALSE;
+
+ GtkWidget *t = GTK_WIDGET(gobj());
+
+ /* Create components */
+ for (i = 0; i < static_cast<gint>(G_N_ELEMENTS(_a)); i++) {
+ /* Label */
+ _l[i] = gtk_label_new("");
+
+ gtk_widget_set_halign(_l[i], GTK_ALIGN_START);
+ gtk_widget_show(_l[i]);
+
+ gtk_widget_set_margin_start(_l[i], XPAD);
+ gtk_widget_set_margin_end(_l[i], XPAD);
+ gtk_widget_set_margin_top(_l[i], YPAD);
+ gtk_widget_set_margin_bottom(_l[i], YPAD);
+ gtk_grid_attach(GTK_GRID(t), _l[i], 0, i, 1, 1);
+
+ /* Adjustment */
+ _a[i] = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, _rangeLimit, 1.0, 10.0, 10.0));
+ /* Slider */
+ _s[i] = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_a[i], true)));
+ _s[i]->show();
+
+ _s[i]->set_margin_start(XPAD);
+ _s[i]->set_margin_end(XPAD);
+ _s[i]->set_margin_top(YPAD);
+ _s[i]->set_margin_bottom(YPAD);
+ _s[i]->set_hexpand(true);
+ gtk_grid_attach(GTK_GRID(t), _s[i]->gobj(), 1, i, 1, 1);
+
+ /* Spinbutton */
+ _b[i] = gtk_spin_button_new(GTK_ADJUSTMENT(_a[i]), 1.0, 0);
+ sp_dialog_defocus_on_enter(_b[i]);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_l[i]), _b[i]);
+ gtk_widget_show(_b[i]);
+
+ gtk_widget_set_margin_start(_b[i], XPAD);
+ gtk_widget_set_margin_end(_b[i], XPAD);
+ gtk_widget_set_margin_top(_b[i], YPAD);
+ gtk_widget_set_margin_bottom(_b[i], YPAD);
+ gtk_widget_set_halign(_b[i], GTK_ALIGN_END);
+ gtk_widget_set_valign(_b[i], GTK_ALIGN_CENTER);
+ gtk_grid_attach(GTK_GRID(t), _b[i], 2, i, 1, 1);
+
+ /* Attach channel value to adjustment */
+ g_object_set_data(G_OBJECT(_a[i]), "channel", GINT_TO_POINTER(i));
+ /* Signals */
+ g_signal_connect(G_OBJECT(_a[i]), "value_changed", G_CALLBACK(_adjustmentAnyChanged), this);
+ _s[i]->signal_grabbed.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyGrabbed));
+ _s[i]->signal_released.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyReleased));
+ _s[i]->signal_value_changed.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyChanged));
+ }
+
+ //Prevent 5th bar from being shown by PanelDialog::show_all_children
+ gtk_widget_set_no_show_all(_l[4], TRUE);
+ _s[4]->set_no_show_all(true);
+ gtk_widget_set_no_show_all(_b[4], TRUE);
+
+ /* Initial mode is none, so it works */
+ setMode(mode);
+}
+
+void ColorScales::_recalcColor()
+{
+ SPColor color;
+ gfloat alpha = 1.0;
+ gfloat c[5];
+
+ switch (_mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ case SP_COLOR_SCALES_MODE_HSL:
+ case SP_COLOR_SCALES_MODE_HSV:
+ _getRgbaFloatv(c);
+ color.set(c[0], c[1], c[2]);
+ alpha = c[3];
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK: {
+ _getCmykaFloatv(c);
+
+ float rgb[3];
+ SPColor::cmyk_to_rgb_floatv(rgb, c[0], c[1], c[2], c[3]);
+ color.set(rgb[0], rgb[1], rgb[2]);
+ alpha = c[4];
+ break;
+ }
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode %d", __FILE__, __LINE__, _mode);
+ break;
+ }
+
+ _color.preserveICC();
+ _color.setColorAlpha(color, alpha);
+}
+
+void ColorScales::_updateDisplay()
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f)", this, _color.color().v.c[0],
+ _color.color().v.c[1], _color.color().v.c[2], _color.alpha());
+#endif
+ gfloat tmp[3];
+ gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 };
+
+ SPColor color = _color.color();
+
+ switch (_mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ color.get_rgb_floatv(c);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ break;
+ case SP_COLOR_SCALES_MODE_HSL:
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsl_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ break;
+ case SP_COLOR_SCALES_MODE_HSV:
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsv_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK:
+ color.get_cmyk_floatv(c);
+ c[4] = _color.alpha();
+ break;
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode %d", __FILE__, __LINE__, _mode);
+ break;
+ }
+
+ _updating = TRUE;
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], c[3]);
+ setScaled(_a[4], c[4]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = FALSE;
+}
+
+/* Helpers for setting color value */
+gfloat ColorScales::getScaled(const GtkAdjustment *a)
+{
+ gfloat val = gtk_adjustment_get_value(const_cast<GtkAdjustment *>(a)) /
+ gtk_adjustment_get_upper(const_cast<GtkAdjustment *>(a));
+ return val;
+}
+
+void ColorScales::setScaled(GtkAdjustment *a, gfloat v, bool constrained)
+{
+ gdouble upper = gtk_adjustment_get_upper(a);
+ gfloat val = v * upper;
+ if (constrained) {
+ // TODO: do we want preferences for these?
+ if (upper == 255) {
+ val = round(val/16) * 16;
+ } else {
+ val = round(val/10) * 10;
+ }
+ }
+ gtk_adjustment_set_value(a, val);
+}
+
+void ColorScales::_setRangeLimit(gdouble upper)
+{
+ _rangeLimit = upper;
+ for (auto & i : _a) {
+ gtk_adjustment_set_upper(i, upper);
+ }
+}
+
+void ColorScales::_onColorChanged()
+{
+ if (!get_visible()) {
+ return;
+ }
+ _updateDisplay();
+}
+
+void ColorScales::on_show()
+{
+ Gtk::Grid::on_show();
+ _updateDisplay();
+}
+
+void ColorScales::_getRgbaFloatv(gfloat *rgba)
+{
+ g_return_if_fail(rgba != nullptr);
+
+ switch (_mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ rgba[0] = getScaled(_a[0]);
+ rgba[1] = getScaled(_a[1]);
+ rgba[2] = getScaled(_a[2]);
+ rgba[3] = getScaled(_a[3]);
+ break;
+ case SP_COLOR_SCALES_MODE_HSL:
+ SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ break;
+ case SP_COLOR_SCALES_MODE_HSV:
+ SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK:
+ SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]));
+ rgba[3] = getScaled(_a[4]);
+ break;
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ break;
+ }
+}
+
+void ColorScales::_getCmykaFloatv(gfloat *cmyka)
+{
+ gfloat rgb[3];
+
+ g_return_if_fail(cmyka != nullptr);
+
+ switch (_mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ cmyka[4] = getScaled(_a[3]);
+ break;
+ case SP_COLOR_SCALES_MODE_HSL:
+ SPColor::hsl_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]);
+ cmyka[4] = getScaled(_a[3]);
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK:
+ cmyka[0] = getScaled(_a[0]);
+ cmyka[1] = getScaled(_a[1]);
+ cmyka[2] = getScaled(_a[2]);
+ cmyka[3] = getScaled(_a[3]);
+ cmyka[4] = getScaled(_a[4]);
+ break;
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ break;
+ }
+}
+
+guint32 ColorScales::_getRgba32()
+{
+ gfloat c[4];
+ guint32 rgba;
+
+ _getRgbaFloatv(c);
+
+ rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]);
+
+ return rgba;
+}
+
+void ColorScales::setMode(SPColorScalesMode mode)
+{
+ gfloat rgba[4];
+ gfloat c[4];
+
+ if (_mode == mode)
+ return;
+
+ if ((_mode == SP_COLOR_SCALES_MODE_RGB) || (_mode == SP_COLOR_SCALES_MODE_HSL) ||
+ (_mode == SP_COLOR_SCALES_MODE_CMYK) || (_mode == SP_COLOR_SCALES_MODE_HSV)) {
+ _getRgbaFloatv(rgba);
+ }
+ else {
+ rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0;
+ }
+
+ _mode = mode;
+
+ switch (mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ _setRangeLimit(255.0);
+ gtk_adjustment_set_upper(_a[3], 100.0);
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_R:"));
+ _s[0]->set_tooltip_text(_("Red"));
+ gtk_widget_set_tooltip_text(_b[0], _("Red"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_G:"));
+ _s[1]->set_tooltip_text(_("Green"));
+ gtk_widget_set_tooltip_text(_b[1], _("Green"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_B:"));
+ _s[2]->set_tooltip_text(_("Blue"));
+ gtk_widget_set_tooltip_text(_b[2], _("Blue"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)"));
+ _s[0]->setMap(nullptr);
+ gtk_widget_hide(_l[4]);
+ _s[4]->hide();
+ gtk_widget_hide(_b[4]);
+ _updating = TRUE;
+ setScaled(_a[0], rgba[0]);
+ setScaled(_a[1], rgba[1]);
+ setScaled(_a[2], rgba[2]);
+ setScaled(_a[3], rgba[3]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = FALSE;
+ break;
+ case SP_COLOR_SCALES_MODE_HSL:
+ _setRangeLimit(100.0);
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ gtk_widget_set_tooltip_text(_b[0], _("Hue"));
+ gtk_adjustment_set_upper(_a[0], 360.0);
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ gtk_widget_set_tooltip_text(_b[1], _("Saturation"));
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_L:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ gtk_widget_set_tooltip_text(_b[2], _("Lightness"));
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)"));
+ _s[0]->setMap((guchar *)(sp_color_scales_hue_map()));
+ gtk_widget_hide(_l[4]);
+ _s[4]->hide();
+ gtk_widget_hide(_b[4]);
+ _updating = TRUE;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]);
+
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = FALSE;
+ break;
+ case SP_COLOR_SCALES_MODE_HSV:
+ _setRangeLimit(100.0);
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ gtk_widget_set_tooltip_text(_b[0], _("Hue"));
+ gtk_adjustment_set_upper(_a[0], 360.0);
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ gtk_widget_set_tooltip_text(_b[1], _("Saturation"));
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_V:"));
+ _s[2]->set_tooltip_text(_("Value"));
+ gtk_widget_set_tooltip_text(_b[2], _("Value"));
+
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)"));
+ _s[0]->setMap((guchar *)(sp_color_scales_hue_map()));
+ gtk_widget_hide(_l[4]);
+ _s[4]->hide();
+ gtk_widget_hide(_b[4]);
+ _updating = TRUE;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]);
+
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = FALSE;
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK:
+ _setRangeLimit(100.0);
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_C:"));
+ _s[0]->set_tooltip_text(_("Cyan"));
+ gtk_widget_set_tooltip_text(_b[0], _("Cyan"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_M:"));
+ _s[1]->set_tooltip_text(_("Magenta"));
+ gtk_widget_set_tooltip_text(_b[1], _("Magenta"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_Y:"));
+ _s[2]->set_tooltip_text(_("Yellow"));
+ gtk_widget_set_tooltip_text(_b[2], _("Yellow"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_K:"));
+ _s[3]->set_tooltip_text(_("Black"));
+ gtk_widget_set_tooltip_text(_b[3], _("Black"));
+ gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[4]), _("_A:"));
+ _s[4]->set_tooltip_text(_("Alpha (opacity)"));
+ gtk_widget_set_tooltip_text(_b[4], _("Alpha (opacity)"));
+ _s[0]->setMap(nullptr);
+ gtk_widget_show(_l[4]);
+ _s[4]->show();
+ gtk_widget_show(_b[4]);
+ _updating = TRUE;
+
+ SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]);
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], c[3]);
+
+ setScaled(_a[4], rgba[3]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = FALSE;
+ break;
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ break;
+ }
+}
+
+SPColorScalesMode ColorScales::getMode() const { return _mode; }
+
+void ColorScales::_adjustmentAnyChanged(GtkAdjustment *adjustment, ColorScales *cs)
+{
+ gint channel = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(adjustment), "channel"));
+
+ _adjustmentChanged(cs, channel);
+}
+
+void ColorScales::_sliderAnyGrabbed()
+{
+ if (_updating) {
+ return;
+ }
+ if (!_dragging) {
+ _dragging = TRUE;
+ _color.setHeld(true);
+ }
+}
+
+void ColorScales::_sliderAnyReleased()
+{
+ if (_updating) {
+ return;
+ }
+ if (_dragging) {
+ _dragging = FALSE;
+ _color.setHeld(false);
+ }
+}
+
+void ColorScales::_sliderAnyChanged()
+{
+ if (_updating) {
+ return;
+ }
+ _recalcColor();
+}
+
+void ColorScales::_adjustmentChanged(ColorScales *scales, guint channel)
+{
+ if (scales->_updating) {
+ return;
+ }
+
+ scales->_updateSliders((1 << channel));
+ scales->_recalcColor();
+}
+
+void ColorScales::_updateSliders(guint channels)
+{
+ gfloat rgb0[3], rgbm[3], rgb1[3];
+#ifdef SPCS_PREVIEW
+ guint32 rgba;
+#endif
+ switch (_mode) {
+ case SP_COLOR_SCALES_MODE_RGB:
+ if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) {
+ /* Update red */
+ _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, getScaled(_a[1]), getScaled(_a[2]), 1.0),
+ SP_RGBA32_F_COMPOSE(0.5, getScaled(_a[1]), getScaled(_a[2]), 1.0),
+ SP_RGBA32_F_COMPOSE(1.0, getScaled(_a[1]), getScaled(_a[2]), 1.0));
+ }
+ if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) {
+ /* Update green */
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 0.0, getScaled(_a[2]), 1.0),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 0.5, getScaled(_a[2]), 1.0),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 1.0, getScaled(_a[2]), 1.0));
+ }
+ if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) {
+ /* Update blue */
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 0.0, 1.0),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 0.5, 1.0),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 1.0, 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.5),
+ SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 1.0));
+ }
+ break;
+ case SP_COLOR_SCALES_MODE_HSL:
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2]));
+ SPColor::hsl_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2]));
+ SPColor::hsl_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2]));
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update value */
+ SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0);
+ SPColor::hsl_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5);
+ SPColor::hsl_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0);
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ break;
+ case SP_COLOR_SCALES_MODE_HSV:
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2]));
+ SPColor::hsv_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2]));
+ SPColor::hsv_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2]));
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update value */
+ SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0);
+ SPColor::hsv_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5);
+ SPColor::hsv_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0);
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ break;
+ case SP_COLOR_SCALES_MODE_CMYK:
+ if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update C */
+ SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]));
+ _s[0]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_M) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update M */
+ SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2]), getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2]), getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2]), getScaled(_a[3]));
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_Y) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update Y */
+ SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0, getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5, getScaled(_a[3]));
+ SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0, getScaled(_a[3]));
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_K) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update K */
+ SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0);
+ SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.5);
+ SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 1.0);
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_CMYKA) {
+ /* Update alpha */
+ SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]),
+ getScaled(_a[3]));
+ _s[4]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ break;
+ default:
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ break;
+ }
+
+#ifdef SPCS_PREVIEW
+ rgba = sp_color_scales_get_rgba32(cs);
+ sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba);
+#endif
+}
+
+static const gchar *sp_color_scales_hue_map()
+{
+ static gchar *map = nullptr;
+
+ if (!map) {
+ gchar *p;
+ gint h;
+ map = g_new(gchar, 4 * 1024);
+ p = map;
+ for (h = 0; h < 1024; h++) {
+ gfloat rgb[3];
+ SPColor::hsl_to_rgb_floatv(rgb, h / 1024.0, 1.0, 0.5);
+ *p++ = SP_COLOR_F_TO_U(rgb[0]);
+ *p++ = SP_COLOR_F_TO_U(rgb[1]);
+ *p++ = SP_COLOR_F_TO_U(rgb[2]);
+ *p++ = 0xFF;
+ }
+ }
+
+ return map;
+}
+
+ColorScalesFactory::ColorScalesFactory(SPColorScalesMode submode)
+ : _submode(submode)
+{
+}
+
+ColorScalesFactory::~ColorScalesFactory() = default;
+
+Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorScales(color, _submode));
+ return w;
+}
+
+Glib::ustring ColorScalesFactory::modeName() const {
+ return gettext(ColorScales::SUBMODE_NAMES[_submode]);
+}
+
+}
+}
+}
diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h
new file mode 100644
index 0000000..f3007fd
--- /dev/null
+++ b/src/ui/widget/color-scales.h
@@ -0,0 +1,110 @@
+// 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.
+ */
+#ifndef SEEN_SP_COLOR_SCALES_H
+#define SEEN_SP_COLOR_SCALES_H
+
+#include <gtkmm/grid.h>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+
+enum SPColorScalesMode {
+ SP_COLOR_SCALES_MODE_NONE = 0,
+ SP_COLOR_SCALES_MODE_RGB = 1,
+ SP_COLOR_SCALES_MODE_HSL = 2,
+ SP_COLOR_SCALES_MODE_CMYK = 3,
+ SP_COLOR_SCALES_MODE_HSV = 4
+};
+
+class ColorScales
+ : public Gtk::Grid
+{
+public:
+ static const gchar *SUBMODE_NAMES[];
+
+ static gfloat getScaled(const GtkAdjustment *a);
+ static void setScaled(GtkAdjustment *a, gfloat v, bool constrained = false);
+
+ ColorScales(SelectedColor &color, SPColorScalesMode mode);
+ ~ColorScales() override;
+
+ virtual void _initUI(SPColorScalesMode mode);
+
+ void setMode(SPColorScalesMode mode);
+ SPColorScalesMode getMode() const;
+
+protected:
+ void _onColorChanged();
+ void on_show() override;
+
+ static void _adjustmentAnyChanged(GtkAdjustment *adjustment, ColorScales *cs);
+ void _sliderAnyGrabbed();
+ void _sliderAnyReleased();
+ void _sliderAnyChanged();
+ static void _adjustmentChanged(ColorScales *cs, guint channel);
+
+ void _getRgbaFloatv(gfloat *rgba);
+ void _getCmykaFloatv(gfloat *cmyka);
+ guint32 _getRgba32();
+ void _updateSliders(guint channels);
+ void _recalcColor();
+ void _updateDisplay();
+
+ void _setRangeLimit(gdouble upper);
+
+ SelectedColor &_color;
+ SPColorScalesMode _mode;
+ gdouble _rangeLimit;
+ gboolean _updating : 1;
+ gboolean _dragging : 1;
+ GtkAdjustment *_a[5]; /* Channel adjustments */
+ Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */
+ GtkWidget *_b[5]; /* Spinbuttons */
+ GtkWidget *_l[5]; /* Labels */
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorScales(ColorScales const &obj) = delete;
+ ColorScales &operator=(ColorScales const &obj) = delete;
+};
+
+class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory
+{
+public:
+ ColorScalesFactory(SPColorScalesMode submode);
+ ~ColorScalesFactory() override;
+
+ Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+
+private:
+ SPColorScalesMode _submode;
+};
+
+}
+}
+}
+
+#endif /* !SEEN_SP_COLOR_SCALES_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/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp
new file mode 100644
index 0000000..2d19055
--- /dev/null
+++ b/src/ui/widget/color-slider.cpp
@@ -0,0 +1,536 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A slider with colored background - implementation.
+ *//*
+ * Authors:
+ * see git history
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/cursor.h>
+#include <gdkmm/general.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/stylecontext.h>
+
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "preferences.h"
+
+static const gint SLIDER_WIDTH = 96;
+static const gint SLIDER_HEIGHT = 8;
+static const gint ARROW_SIZE = 7;
+
+static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[],
+ guint b0, guint b1, guint mask);
+static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start,
+ gint step, guint b0, guint b1, guint mask);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorSlider::ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment)
+ : _dragging(false)
+ , _value(0.0)
+ , _oldvalue(0.0)
+ , _mapsize(0)
+ , _map(nullptr)
+{
+ _c0[0] = 0x00;
+ _c0[1] = 0x00;
+ _c0[2] = 0x00;
+ _c0[3] = 0xff;
+
+ _cm[0] = 0xff;
+ _cm[1] = 0x00;
+ _cm[2] = 0x00;
+ _cm[3] = 0xff;
+
+ _c0[0] = 0xff;
+ _c0[1] = 0xff;
+ _c0[2] = 0xff;
+ _c0[3] = 0xff;
+
+ _b0 = 0x5f;
+ _b1 = 0xa0;
+ _bmask = 0x08;
+
+ setAdjustment(adjustment);
+}
+
+ColorSlider::~ColorSlider()
+{
+ if (_adjustment) {
+ _adjustment_changed_connection.disconnect();
+ _adjustment_value_changed_connection.disconnect();
+ _adjustment.reset();
+ }
+}
+
+void ColorSlider::on_realize()
+{
+ set_realized();
+
+ if (!_gdk_window) {
+ GdkWindowAttr attributes;
+ gint attributes_mask;
+ Gtk::Allocation allocation = get_allocation();
+
+ memset(&attributes, 0, sizeof(attributes));
+ attributes.x = allocation.get_x();
+ attributes.y = allocation.get_y();
+ attributes.width = allocation.get_width();
+ attributes.height = allocation.get_height();
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default());
+ attributes.event_mask = get_events();
+ attributes.event_mask |= (Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
+
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ _gdk_window = Gdk::Window::create(get_parent_window(), &attributes, attributes_mask);
+ set_window(_gdk_window);
+ _gdk_window->set_user_data(gobj());
+ }
+}
+
+void ColorSlider::on_unrealize()
+{
+ _gdk_window.reset();
+
+ Gtk::Widget::on_unrealize();
+}
+
+void ColorSlider::on_size_allocate(Gtk::Allocation &allocation)
+{
+ set_allocation(allocation);
+
+ if (get_realized()) {
+ _gdk_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(),
+ allocation.get_height());
+ }
+}
+
+void ColorSlider::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border padding = style_context->get_padding(get_state_flags());
+ int width = SLIDER_WIDTH + padding.get_left() + padding.get_right();
+ minimum_width = natural_width = width;
+}
+
+void ColorSlider::get_preferred_width_for_height_vfunc(int /*height*/, int &minimum_width, int &natural_width) const
+{
+ get_preferred_width(minimum_width, natural_width);
+}
+
+void ColorSlider::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border padding = style_context->get_padding(get_state_flags());
+ int height = SLIDER_HEIGHT + padding.get_top() + padding.get_bottom();
+ minimum_height = natural_height = height;
+}
+
+void ColorSlider::get_preferred_height_for_width_vfunc(int /*width*/, int &minimum_height, int &natural_height) const
+{
+ get_preferred_height(minimum_height, natural_height);
+}
+
+bool ColorSlider::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ Gtk::Allocation allocation = get_allocation();
+ gint cx, cw;
+ cx = get_style_context()->get_padding(get_state_flags()).get_left();
+ cw = allocation.get_width() - 2 * cx;
+ signal_grabbed.emit();
+ _dragging = true;
+ _oldvalue = _value;
+ gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0);
+ bool constrained = event->state & GDK_CONTROL_MASK;
+ ColorScales::setScaled(_adjustment->gobj(), value, constrained);
+ signal_dragged.emit();
+
+ auto window = _gdk_window->gobj();
+
+ auto seat = gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event));
+ gdk_seat_grab(seat,
+ window,
+ GDK_SEAT_CAPABILITY_ALL_POINTING,
+ FALSE,
+ nullptr,
+ reinterpret_cast<GdkEvent *>(event),
+ nullptr,
+ nullptr);
+ }
+
+ return false;
+}
+
+bool ColorSlider::on_button_release_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ gdk_seat_ungrab(gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event)));
+ _dragging = false;
+ signal_released.emit();
+ if (_value != _oldvalue) {
+ signal_value_changed.emit();
+ }
+ }
+
+ return false;
+}
+
+bool ColorSlider::on_motion_notify_event(GdkEventMotion *event)
+{
+ if (_dragging) {
+ gint cx, cw;
+ Gtk::Allocation allocation = get_allocation();
+ cx = get_style_context()->get_padding(get_state_flags()).get_left();
+ cw = allocation.get_width() - 2 * cx;
+ gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0);
+ bool constrained = event->state & GDK_CONTROL_MASK;
+ ColorScales::setScaled(_adjustment->gobj(), value, constrained);
+ signal_dragged.emit();
+ }
+
+ return false;
+}
+
+void ColorSlider::setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment)
+{
+ if (!adjustment) {
+ _adjustment = Gtk::Adjustment::create(0.0, 0.0, 1.0, 0.01, 0.0, 0.0);
+ }
+ else {
+ adjustment->set_page_increment(0.0);
+ adjustment->set_page_size(0.0);
+ }
+
+ if (_adjustment != adjustment) {
+ if (_adjustment) {
+ _adjustment_changed_connection.disconnect();
+ _adjustment_value_changed_connection.disconnect();
+ }
+
+ _adjustment = adjustment;
+ _adjustment_changed_connection =
+ _adjustment->signal_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentChanged));
+ _adjustment_value_changed_connection =
+ _adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentValueChanged));
+
+ _value = ColorScales::getScaled(_adjustment->gobj());
+
+ _onAdjustmentChanged();
+ }
+}
+
+void ColorSlider::_onAdjustmentChanged() { queue_draw(); }
+
+void ColorSlider::_onAdjustmentValueChanged()
+{
+ if (_value != ColorScales::getScaled(_adjustment->gobj())) {
+ gint cx, cy, cw, ch;
+ auto style_context = get_style_context();
+ auto allocation = get_allocation();
+ auto padding = style_context->get_padding(get_state_flags());
+ cx = padding.get_left();
+ cy = padding.get_top();
+ cw = allocation.get_width() - 2 * cx;
+ ch = allocation.get_height() - 2 * cy;
+ if ((gint)(ColorScales::getScaled(_adjustment->gobj()) * cw) != (gint)(_value * cw)) {
+ gint ax, ay;
+ gfloat value;
+ value = _value;
+ _value = ColorScales::getScaled(_adjustment->gobj());
+ ax = (int)(cx + value * cw - ARROW_SIZE / 2 - 2);
+ ay = cy;
+ queue_draw_area(ax, ay, ARROW_SIZE + 4, ch);
+ ax = (int)(cx + _value * cw - ARROW_SIZE / 2 - 2);
+ ay = cy;
+ queue_draw_area(ax, ay, ARROW_SIZE + 4, ch);
+ }
+ else {
+ _value = ColorScales::getScaled(_adjustment->gobj());
+ }
+ }
+}
+
+void ColorSlider::setColors(guint32 start, guint32 mid, guint32 end)
+{
+ // Remove any map, if set
+ _map = nullptr;
+
+ _c0[0] = start >> 24;
+ _c0[1] = (start >> 16) & 0xff;
+ _c0[2] = (start >> 8) & 0xff;
+ _c0[3] = start & 0xff;
+
+ _cm[0] = mid >> 24;
+ _cm[1] = (mid >> 16) & 0xff;
+ _cm[2] = (mid >> 8) & 0xff;
+ _cm[3] = mid & 0xff;
+
+ _c1[0] = end >> 24;
+ _c1[1] = (end >> 16) & 0xff;
+ _c1[2] = (end >> 8) & 0xff;
+ _c1[3] = end & 0xff;
+
+ queue_draw();
+}
+
+void ColorSlider::setMap(const guchar *map)
+{
+ _map = const_cast<guchar *>(map);
+
+ queue_draw();
+}
+
+void ColorSlider::setBackground(guint dark, guint light, guint size)
+{
+ _b0 = dark;
+ _b1 = light;
+ _bmask = size;
+
+ queue_draw();
+}
+
+bool ColorSlider::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ gboolean colorsOnTop = Inkscape::Preferences::get()->getBool("/options/workarounds/colorsontop", false);
+
+ auto allocation = get_allocation();
+ auto style_context = get_style_context();
+
+ // Draw shadow
+ if (colorsOnTop) {
+ style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height());
+ }
+
+ /* Paintable part of color gradient area */
+ Gdk::Rectangle carea;
+ Gtk::Border padding;
+
+ padding = style_context->get_padding(get_state_flags());
+
+ carea.set_x(padding.get_left());
+ carea.set_y(padding.get_top());
+
+ carea.set_width(allocation.get_width() - 2 * carea.get_x());
+ carea.set_height(allocation.get_height() - 2 * carea.get_y());
+
+ if (_map) {
+ /* Render map pixelstore */
+ gint d = (1024 << 16) / carea.get_width();
+ gint s = 0;
+
+ const guchar *b =
+ sp_color_slider_render_map(0, 0, carea.get_width(), carea.get_height(), _map, s, d, _b0, _b1, _bmask);
+
+ if (b != nullptr && carea.get_width() > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb = Gdk::Pixbuf::create_from_data(
+ b, Gdk::COLORSPACE_RGB, false, 8, carea.get_width(), carea.get_height(), carea.get_width() * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+ else {
+ gint c[4], dc[4];
+
+ /* Render gradient */
+
+ // part 1: from c0 to cm
+ if (carea.get_width() > 0) {
+ for (gint i = 0; i < 4; i++) {
+ c[i] = _c0[i] << 16;
+ dc[i] = ((_cm[i] << 16) - c[i]) / (carea.get_width() / 2);
+ }
+ guint wi = carea.get_width() / 2;
+ const guchar *b = sp_color_slider_render_gradient(0, 0, wi, carea.get_height(), c, dc, _b0, _b1, _bmask);
+
+ /* Draw pixelstore 1 */
+ if (b != nullptr && wi > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb =
+ Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+
+ // part 2: from cm to c1
+ if (carea.get_width() > 0) {
+ for (gint i = 0; i < 4; i++) {
+ c[i] = _cm[i] << 16;
+ dc[i] = ((_c1[i] << 16) - c[i]) / (carea.get_width() / 2);
+ }
+ guint wi = carea.get_width() / 2;
+ const guchar *b = sp_color_slider_render_gradient(carea.get_width() / 2, 0, wi, carea.get_height(), c, dc,
+ _b0, _b1, _bmask);
+
+ /* Draw pixelstore 2 */
+ if (b != nullptr && wi > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb =
+ Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_width() / 2 + carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+ }
+
+ /* Draw shadow */
+ if (!colorsOnTop) {
+ style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height());
+ }
+
+ /* Draw arrow */
+ gint x = (int)(_value * (carea.get_width() - 1) - ARROW_SIZE / 2 + carea.get_x());
+ gint y1 = carea.get_y();
+ gint y2 = carea.get_y() + carea.get_height() - 1;
+ cr->set_line_width(1.0);
+
+ // Define top arrow
+ cr->move_to(x - 0.5, y1 + 0.5);
+ cr->line_to(x + ARROW_SIZE - 0.5, y1 + 0.5);
+ cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y1 + ARROW_SIZE / 2.0 + 0.5);
+ cr->line_to(x - 0.5, y1 + 0.5);
+
+ // Define bottom arrow
+ cr->move_to(x - 0.5, y2 + 0.5);
+ cr->line_to(x + ARROW_SIZE - 0.5, y2 + 0.5);
+ cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y2 - ARROW_SIZE / 2.0 + 0.5);
+ cr->line_to(x - 0.5, y2 + 0.5);
+
+ // Render both arrows
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->stroke_preserve();
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->fill();
+
+ return false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/* Colors are << 16 */
+
+static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[],
+ guint b0, guint b1, guint mask)
+{
+ static guchar *buf = nullptr;
+ static gint bs = 0;
+ guchar *dp;
+ gint x, y;
+ guint r, g, b, a;
+
+ if (buf && (bs < width * height)) {
+ g_free(buf);
+ buf = nullptr;
+ }
+ if (!buf) {
+ buf = g_new(guchar, width * height * 3);
+ bs = width * height;
+ }
+
+ dp = buf;
+ r = c[0];
+ g = c[1];
+ b = c[2];
+ a = c[3];
+ for (x = x0; x < x0 + width; x++) {
+ gint cr, cg, cb, ca;
+ guchar *d;
+ cr = r >> 16;
+ cg = g >> 16;
+ cb = b >> 16;
+ ca = a >> 16;
+ d = dp;
+ for (y = y0; y < y0 + height; y++) {
+ guint bg, fc;
+ /* Background value */
+ bg = ((x & mask) ^ (y & mask)) ? b0 : b1;
+ fc = (cr - bg) * ca;
+ d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cg - bg) * ca;
+ d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cb - bg) * ca;
+ d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ d += 3 * width;
+ }
+ r += dc[0];
+ g += dc[1];
+ b += dc[2];
+ a += dc[3];
+ dp += 3;
+ }
+
+ return buf;
+}
+
+/* Positions are << 16 */
+
+static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start,
+ gint step, guint b0, guint b1, guint mask)
+{
+ static guchar *buf = nullptr;
+ static gint bs = 0;
+ guchar *dp;
+ gint x, y;
+
+ if (buf && (bs < width * height)) {
+ g_free(buf);
+ buf = nullptr;
+ }
+ if (!buf) {
+ buf = g_new(guchar, width * height * 3);
+ bs = width * height;
+ }
+
+ dp = buf;
+ for (x = x0; x < x0 + width; x++) {
+ gint cr, cg, cb, ca;
+ guchar *d = dp;
+ guchar *sp = map + 4 * (start >> 16);
+ cr = *sp++;
+ cg = *sp++;
+ cb = *sp++;
+ ca = *sp++;
+ for (y = y0; y < y0 + height; y++) {
+ guint bg, fc;
+ /* Background value */
+ bg = ((x & mask) ^ (y & mask)) ? b0 : b1;
+ fc = (cr - bg) * ca;
+ d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cg - bg) * ca;
+ d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cb - bg) * ca;
+ d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ d += 3 * width;
+ }
+ dp += 3;
+ start += step;
+ }
+
+ return buf;
+}
+/*
+ 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/widget/color-slider.h b/src/ui/widget/color-slider.h
new file mode 100644
index 0000000..6a0834e
--- /dev/null
+++ b/src/ui/widget/color-slider.h
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors:
+ * see git history
+* Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_SLIDER_H
+#define SEEN_COLOR_SLIDER_H
+
+#include <gtkmm/widget.h>
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * A slider with colored background
+ */
+class ColorSlider : public Gtk::Widget {
+public:
+ ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment);
+ ~ColorSlider() override;
+
+ void setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment);
+
+ void setColors(guint32 start, guint32 mid, guint32 end);
+
+ void setMap(const guchar *map);
+
+ void setBackground(guint dark, guint light, guint size);
+
+ sigc::signal<void> signal_grabbed;
+ sigc::signal<void> signal_dragged;
+ sigc::signal<void> signal_released;
+ sigc::signal<void> signal_value_changed;
+
+protected:
+ void on_size_allocate(Gtk::Allocation &allocation) override;
+ void on_realize() override;
+ void on_unrealize() override;
+ bool on_button_press_event(GdkEventButton *event) override;
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion *event) override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+ void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
+ void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
+ void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
+
+private:
+ void _onAdjustmentChanged();
+ void _onAdjustmentValueChanged();
+
+ bool _dragging;
+
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ sigc::connection _adjustment_changed_connection;
+ sigc::connection _adjustment_value_changed_connection;
+
+ gfloat _value;
+ gfloat _oldvalue;
+ guchar _c0[4], _cm[4], _c1[4];
+ guchar _b0, _b1;
+ guchar _bmask;
+
+ gint _mapsize;
+ guchar *_map;
+
+ Glib::RefPtr<Gdk::Window> _gdk_window;
+};
+
+} // namespace Widget
+} // 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/widget/color-wheel-selector.cpp b/src/ui/widget/color-wheel-selector.cpp
new file mode 100644
index 0000000..9695303
--- /dev/null
+++ b/src/ui/widget/color-wheel-selector.cpp
@@ -0,0 +1,237 @@
+// 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 "color-wheel-selector.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/label.h>
+#include <gtkmm/spinbutton.h>
+#include "ui/dialog-events.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/ink-color-wheel.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+#define XPAD 4
+#define YPAD 1
+
+
+const gchar *ColorWheelSelector::MODE_NAME = N_("Wheel");
+
+ColorWheelSelector::ColorWheelSelector(SelectedColor &color)
+ : Gtk::Grid()
+ , _color(color)
+ , _updating(false)
+ , _wheel(nullptr)
+ , _slider(nullptr)
+{
+ set_name("ColorWheelSelector");
+
+ _initUI();
+ _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorWheelSelector::_colorChanged));
+ _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorWheelSelector::_colorChanged));
+}
+
+ColorWheelSelector::~ColorWheelSelector()
+{
+ _color_changed_connection.disconnect();
+ _color_dragged_connection.disconnect();
+}
+
+void ColorWheelSelector::_initUI()
+{
+ /* Create components */
+ gint row = 0;
+
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheel());
+ _wheel->set_halign(Gtk::ALIGN_FILL);
+ _wheel->set_valign(Gtk::ALIGN_FILL);
+ _wheel->set_hexpand(true);
+ _wheel->set_vexpand(true);
+ attach(*_wheel, 0, row, 3, 1);
+
+ row++;
+
+ /* Label */
+ Gtk::Label *label = Gtk::manage(new Gtk::Label(_("_A:"), true));
+ label->set_halign(Gtk::ALIGN_END);
+ label->set_valign(Gtk::ALIGN_CENTER);
+
+ label->set_margin_start(XPAD);
+ label->set_margin_end(XPAD);
+ label->set_margin_top(YPAD);
+ label->set_margin_bottom(YPAD);
+ label->set_halign(Gtk::ALIGN_FILL);
+ label->set_valign(Gtk::ALIGN_FILL);
+ attach(*label, 0, row, 1, 1);
+
+ /* Adjustment */
+ _alpha_adjustment = Gtk::Adjustment::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0);
+
+ /* Slider */
+ _slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_alpha_adjustment));
+ _slider->set_tooltip_text(_("Alpha (opacity)"));
+
+ _slider->set_margin_start(XPAD);
+ _slider->set_margin_end(XPAD);
+ _slider->set_margin_top(YPAD);
+ _slider->set_margin_bottom(YPAD);
+ _slider->set_hexpand(true);
+ _slider->set_halign(Gtk::ALIGN_FILL);
+ _slider->set_valign(Gtk::ALIGN_FILL);
+ attach(*_slider, 1, row, 1, 1);
+
+ _slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5),
+ SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0));
+
+ /* Spinbutton */
+ auto spin_button = Gtk::manage(new Gtk::SpinButton(_alpha_adjustment, 1.0, 0));
+ spin_button->set_tooltip_text(_("Alpha (opacity)"));
+ sp_dialog_defocus_on_enter(GTK_WIDGET(spin_button->gobj()));
+ label->set_mnemonic_widget(*spin_button);
+
+ spin_button->set_margin_start(XPAD);
+ spin_button->set_margin_end(XPAD);
+ spin_button->set_margin_top(YPAD);
+ spin_button->set_margin_bottom(YPAD);
+ spin_button->set_halign(Gtk::ALIGN_CENTER);
+ spin_button->set_valign(Gtk::ALIGN_CENTER);
+ attach(*spin_button, 2, row, 1, 1);
+
+ /* Signals */
+ _alpha_adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorWheelSelector::_adjustmentChanged));
+ _slider->signal_grabbed.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderGrabbed));
+ _slider->signal_released.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderReleased));
+ _slider->signal_value_changed.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderChanged));
+ _wheel->signal_color_changed().connect(sigc::mem_fun(*this, &ColorWheelSelector::_wheelChanged));
+
+ show_all();
+}
+
+void ColorWheelSelector::on_show()
+{
+ Gtk::Grid::on_show();
+ _updateDisplay();
+}
+
+void ColorWheelSelector::_colorChanged()
+{
+ _updateDisplay();
+}
+
+void ColorWheelSelector::_adjustmentChanged()
+{
+ if (_updating) {
+ return;
+ }
+
+ _color.preserveICC();
+ _color.setAlpha(ColorScales::getScaled(_alpha_adjustment->gobj()));
+}
+
+void ColorWheelSelector::_sliderGrabbed()
+{
+ _color.preserveICC();
+ _color.setHeld(true);
+}
+
+void ColorWheelSelector::_sliderReleased()
+{
+ _color.preserveICC();
+ _color.setHeld(false);
+}
+
+void ColorWheelSelector::_sliderChanged()
+{
+ if (_updating) {
+ return;
+ }
+
+ _color.preserveICC();
+ _color.setAlpha(ColorScales::getScaled(_alpha_adjustment->gobj()));
+}
+
+void ColorWheelSelector::_wheelChanged()
+{
+ if (_updating) {
+ return;
+ }
+
+ double rgb[3] = { 0, 0, 0 };
+ _wheel->get_rgb(rgb[0], rgb[1], rgb[2]);
+
+ SPColor color(rgb[0], rgb[1], rgb[2]);
+
+ guint32 start = color.toRGBA32(0x00);
+ guint32 mid = color.toRGBA32(0x7f);
+ guint32 end = color.toRGBA32(0xff);
+
+ _updating = true;
+ _slider->setColors(start, mid, end);
+ _color.preserveICC();
+
+ _color.setHeld(_wheel->is_adjusting());
+ _color.setColor(color);
+ _updating = false;
+}
+
+void ColorWheelSelector::_updateDisplay()
+{
+ if(_updating) { return; }
+
+#ifdef DUMP_CHANGE_INFO
+ g_message("ColorWheelSelector::_colorChanged( this=%p, %f, %f, %f, %f)", this, _color.color().v.c[0],
+ _color.color().v.c[1], _color.color().v.c[2], alpha);
+#endif
+
+ _updating = true;
+ {
+ float hsv[3] = { 0, 0, 0 };
+ SPColor::rgb_to_hsv_floatv(hsv, _color.color().v.c[0], _color.color().v.c[1], _color.color().v.c[2]);
+ _wheel->set_rgb(_color.color().v.c[0], _color.color().v.c[1], _color.color().v.c[2]);
+ }
+
+ guint32 start = _color.color().toRGBA32(0x00);
+ guint32 mid = _color.color().toRGBA32(0x7f);
+ guint32 end = _color.color().toRGBA32(0xff);
+
+ _slider->setColors(start, mid, end);
+
+ ColorScales::setScaled(_alpha_adjustment->gobj(), _color.alpha());
+
+ _updating = false;
+}
+
+
+Gtk::Widget *ColorWheelSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorWheelSelector(color));
+ return w;
+}
+
+Glib::ustring ColorWheelSelectorFactory::modeName() const { return gettext(ColorWheelSelector::MODE_NAME); }
+}
+}
+}
+/*
+ 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/widget/color-wheel-selector.h b/src/ui/widget/color-wheel-selector.h
new file mode 100644
index 0000000..59cf6b6
--- /dev/null
+++ b/src/ui/widget/color-wheel-selector.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Color selector widget containing GIMP color wheel and slider
+ */
+/* Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_WHEEL_SELECTOR_H
+#define SEEN_SP_COLOR_WHEEL_SELECTOR_H
+
+#include <gtkmm/grid.h>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+class ColorWheel;
+class ColorWheelSelector
+ : public Gtk::Grid
+{
+public:
+ static const gchar *MODE_NAME;
+
+ ColorWheelSelector(SelectedColor &color);
+ ~ColorWheelSelector() override;
+
+protected:
+ void _initUI();
+
+ void on_show() override;
+
+ void _colorChanged();
+ void _adjustmentChanged();
+ void _sliderGrabbed();
+ void _sliderReleased();
+ void _sliderChanged();
+ void _wheelChanged();
+
+ void _updateDisplay();
+
+ SelectedColor &_color;
+ bool _updating;
+ Glib::RefPtr<Gtk::Adjustment> _alpha_adjustment;
+ Inkscape::UI::Widget::ColorWheel *_wheel;
+ Inkscape::UI::Widget::ColorSlider *_slider;
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorWheelSelector(const ColorWheelSelector &obj) = delete;
+ ColorWheelSelector &operator=(const ColorWheelSelector &obj) = delete;
+
+ sigc::connection _color_changed_connection;
+ sigc::connection _color_dragged_connection;
+};
+
+class ColorWheelSelectorFactory : public ColorSelectorFactory {
+public:
+ Gtk::Widget *createWidget(SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+};
+}
+}
+}
+
+#endif // SEEN_SP_COLOR_WHEEL_SELECTOR_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/widget/combo-box-entry-tool-item.cpp b/src/ui/widget/combo-box-entry-tool-item.cpp
new file mode 100644
index 0000000..9adcca1
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.cpp
@@ -0,0 +1,691 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry.
+ * Features:
+ * Setting GtkEntryBox width in characters.
+ * Passing a function for formatting cells.
+ * Displaying a warning if entry text isn't in list.
+ * Check comma separated values in text against list. (Useful for font-family fallbacks.)
+ * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry)
+ * to allow setting resources.
+ *
+ * Author(s):
+ * Tavmjong Bah
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * We must provide for both a toolbar item and a menu item.
+ * As we don't know which widgets are used (or even constructed),
+ * we must keep track of things like active entry ourselves.
+ */
+
+#include "combo-box-entry-tool-item.h"
+
+#include <iostream>
+#include <cstring>
+#include <glibmm/ustring.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm/display.h>
+
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ComboBoxEntryToolItem::ComboBoxEntryToolItem(Glib::ustring name,
+ Glib::ustring label,
+ Glib::ustring tooltip,
+ GtkTreeModel *model,
+ gint entry_width,
+ gint extra_width,
+ void *cell_data_func,
+ void *separator_func,
+ GtkWidget *focusWidget)
+ : _label(std::move(label)),
+ _tooltip(std::move(tooltip)),
+ _model(model),
+ _entry_width(entry_width),
+ _extra_width(extra_width),
+ _cell_data_func(cell_data_func),
+ _separator_func(separator_func),
+ _focusWidget(focusWidget),
+ _active(-1),
+ _text(strdup("")),
+ _entry_completion(nullptr),
+ _indicator(nullptr),
+ _popup(false),
+ _info(nullptr),
+ _info_cb(nullptr),
+ _info_cb_id(0),
+ _info_cb_blocked(false),
+ _warning(nullptr),
+ _warning_cb(nullptr),
+ _warning_cb_id(0),
+ _warning_cb_blocked(false),
+ _altx_name(nullptr)
+{
+ set_name(name);
+
+ gchar *action_name = g_strdup( get_name().c_str() );
+ gchar *combobox_name = g_strjoin( nullptr, action_name, "_combobox", NULL );
+ gchar *entry_name = g_strjoin( nullptr, action_name, "_entry", NULL );
+ g_free( action_name );
+
+ GtkWidget* comboBoxEntry = gtk_combo_box_new_with_model_and_entry (_model);
+ gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (comboBoxEntry), 0);
+
+ // Name it so we can muck with it using an RC file
+ gtk_widget_set_name( comboBoxEntry, combobox_name );
+ g_free( combobox_name );
+
+ {
+ gtk_widget_set_halign(comboBoxEntry, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(comboBoxEntry, FALSE);
+ gtk_widget_set_vexpand(comboBoxEntry, FALSE);
+ add(*Glib::wrap(comboBoxEntry));
+ }
+
+ _combobox = GTK_COMBO_BOX (comboBoxEntry);
+
+ //gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), ink_comboboxentry_action->active );
+ gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), 0 );
+
+ g_signal_connect( G_OBJECT(comboBoxEntry), "changed", G_CALLBACK(combo_box_changed_cb), this );
+
+ // Optionally add separator function...
+ if( _separator_func != nullptr ) {
+ gtk_combo_box_set_row_separator_func( _combobox,
+ GtkTreeViewRowSeparatorFunc (_separator_func),
+ nullptr, nullptr );
+ }
+
+ // FIXME: once gtk3 migration is done this can be removed
+ // https://bugzilla.gnome.org/show_bug.cgi?id=734915
+ gtk_widget_show_all (comboBoxEntry);
+
+ // Optionally add formatting...
+ if( _cell_data_func != nullptr ) {
+ GtkCellRenderer *cell = gtk_cell_renderer_text_new();
+ gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) );
+ gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), cell, true );
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), cell,
+ GtkCellLayoutDataFunc (_cell_data_func),
+ nullptr, nullptr );
+ }
+
+ // Optionally widen the combobox width... which widens the drop-down list in list mode.
+ if( _extra_width > 0 ) {
+ GtkRequisition req;
+ gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr);
+ gtk_widget_set_size_request( GTK_WIDGET( _combobox ),
+ req.width + _extra_width, -1 );
+ }
+
+ // Get reference to GtkEntry and fiddle a bit with it.
+ GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) );
+
+ // Name it so we can muck with it using an RC file
+ gtk_widget_set_name( child, entry_name );
+ g_free( entry_name );
+
+ if( child && GTK_IS_ENTRY( child ) ) {
+
+ _entry = GTK_ENTRY(child);
+
+ // Change width
+ if( _entry_width > 0 ) {
+ gtk_entry_set_width_chars (GTK_ENTRY (child), _entry_width );
+ }
+
+ // Add pop-up entry completion if required
+ if( _popup ) {
+ popup_enable();
+ }
+
+ // Add altx_name if required
+ if( _altx_name ) {
+ g_object_set_data( G_OBJECT( child ), _altx_name, _entry );
+ }
+
+ // Add signal for GtkEntry to check if finished typing.
+ g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(entry_activate_cb), this );
+ g_signal_connect( G_OBJECT(child), "key-press-event", G_CALLBACK(keypress_cb), this );
+ }
+
+ set_tooltip(_tooltip.c_str());
+
+ show_all();
+}
+
+// Setters/Getters ---------------------------------------------------
+
+gchar*
+ComboBoxEntryToolItem::get_active_text()
+{
+ gchar* text = g_strdup( _text );
+ return text;
+}
+
+/*
+ * For the font-family list we need to handle two cases:
+ * Text is in list store:
+ * In this case we use row number as the font-family list can have duplicate
+ * entries, one in the document font part and one in the system font part. In
+ * order that scrolling through the list works properly we must distinguish
+ * between the two.
+ * Text is not in the list store (i.e. default font-family is not on system):
+ * In this case we have a row number of -1, and the text must be set by hand.
+ */
+gboolean
+ComboBoxEntryToolItem::set_active_text(const gchar* text, int row)
+{
+ if( strcmp( _text, text ) != 0 ) {
+ g_free( _text );
+ _text = g_strdup( text );
+ }
+
+ // Get active row or -1 if none
+ if( row < 0 ) {
+ row = get_active_row_from_text(this, _text);
+ }
+ _active = row;
+
+ // Set active row, check that combobox has been created.
+ if( _combobox ) {
+ gtk_combo_box_set_active( GTK_COMBO_BOX( _combobox ), _active );
+ }
+
+ // Fiddle with entry
+ if( _entry ) {
+
+ // Explicitly set text in GtkEntry box (won't be set if text not in list).
+ gtk_entry_set_text( _entry, text );
+
+ // Show or hide warning -- this might be better moved to text-toolbox.cpp
+ if( _info_cb_id != 0 &&
+ !_info_cb_blocked ) {
+ g_signal_handler_block (G_OBJECT(_entry),
+ _info_cb_id );
+ _info_cb_blocked = true;
+ }
+ if( _warning_cb_id != 0 &&
+ !_warning_cb_blocked ) {
+ g_signal_handler_block (G_OBJECT(_entry),
+ _warning_cb_id );
+ _warning_cb_blocked = true;
+ }
+
+ bool set = false;
+ if( _warning != nullptr ) {
+ Glib::ustring missing = check_comma_separated_text();
+ if( !missing.empty() ) {
+ gtk_entry_set_icon_from_icon_name( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ INKSCAPE_ICON("dialog-warning") );
+ // Can't add tooltip until icon set
+ Glib::ustring warning = _warning;
+ warning += ": ";
+ warning += missing;
+ gtk_entry_set_icon_tooltip_text( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ warning.c_str() );
+
+ if( _warning_cb ) {
+
+ // Add callback if we haven't already
+ if( _warning_cb_id == 0 ) {
+ _warning_cb_id =
+ g_signal_connect( G_OBJECT(_entry),
+ "icon-press",
+ G_CALLBACK(_warning_cb),
+ this);
+ }
+ // Unblock signal
+ if( _warning_cb_blocked ) {
+ g_signal_handler_unblock (G_OBJECT(_entry),
+ _warning_cb_id );
+ _warning_cb_blocked = false;
+ }
+ }
+ set = true;
+ }
+ }
+
+ if( !set && _info != nullptr ) {
+ gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ INKSCAPE_ICON("edit-select-all") );
+ gtk_entry_set_icon_tooltip_text( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ _info );
+
+ if( _info_cb ) {
+ // Add callback if we haven't already
+ if( _info_cb_id == 0 ) {
+ _info_cb_id =
+ g_signal_connect( G_OBJECT(_entry),
+ "icon-press",
+ G_CALLBACK(_info_cb),
+ this);
+ }
+ // Unblock signal
+ if( _info_cb_blocked ) {
+ g_signal_handler_unblock (G_OBJECT(_entry),
+ _info_cb_id );
+ _info_cb_blocked = false;
+ }
+ }
+ set = true;
+ }
+
+ if( !set ) {
+ gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ nullptr );
+ }
+ }
+
+ // Return if active text in list
+ gboolean found = ( _active != -1 );
+ return found;
+}
+
+void
+ComboBoxEntryToolItem::set_entry_width(gint entry_width)
+{
+ _entry_width = entry_width;
+
+ // Clamp to limits
+ if(entry_width < -1) entry_width = -1;
+ if(entry_width > 100) entry_width = 100;
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_width_chars( GTK_ENTRY(_entry), entry_width );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_extra_width( gint extra_width )
+{
+ _extra_width = extra_width;
+
+ // Clamp to limits
+ if(extra_width < -1) extra_width = -1;
+ if(extra_width > 500) extra_width = 500;
+
+ // Widget may not have been created....
+ if( _combobox ) {
+ GtkRequisition req;
+ gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr);
+ gtk_widget_set_size_request( GTK_WIDGET( _combobox ), req.width + _extra_width, -1 );
+ }
+}
+
+void
+ComboBoxEntryToolItem::focus_on_click( bool focus_on_click )
+{
+ if (_combobox) {
+ gtk_widget_set_focus_on_click(GTK_WIDGET(_combobox), focus_on_click);
+ }
+}
+
+void
+ComboBoxEntryToolItem::popup_enable()
+{
+ _popup = true;
+
+ // Widget may not have been created....
+ if( _entry ) {
+
+ // Check we don't already have a GtkEntryCompletion
+ if( _entry_completion ) return;
+
+ _entry_completion = gtk_entry_completion_new();
+
+ gtk_entry_set_completion( _entry, _entry_completion );
+ gtk_entry_completion_set_model( _entry_completion, _model );
+ gtk_entry_completion_set_text_column( _entry_completion, 0 );
+ gtk_entry_completion_set_popup_completion( _entry_completion, true );
+ gtk_entry_completion_set_inline_completion( _entry_completion, false );
+ gtk_entry_completion_set_inline_selection( _entry_completion, true );
+
+ g_signal_connect (G_OBJECT (_entry_completion), "match-selected", G_CALLBACK (match_selected_cb), this);
+ }
+}
+
+void
+ComboBoxEntryToolItem::popup_disable()
+{
+ _popup = false;
+
+ if( _entry_completion ) {
+ gtk_widget_destroy(GTK_WIDGET(_entry_completion));
+ _entry_completion = nullptr;
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_tooltip(const gchar* tooltip)
+{
+ set_tooltip_text(tooltip);
+ gtk_widget_set_tooltip_text ( GTK_WIDGET(_combobox), tooltip);
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_widget_set_tooltip_text ( GTK_WIDGET(_entry), tooltip);
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_info(const gchar* info)
+{
+ g_free( _info );
+ _info = g_strdup( info );
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ _info );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_info_cb(gpointer info_cb)
+{
+ _info_cb = info_cb;
+}
+
+void
+ComboBoxEntryToolItem::set_warning(const gchar* warning)
+{
+ g_free( _warning );
+ _warning = g_strdup( warning );
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ _warning );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_warning_cb(gpointer warning_cb)
+{
+ _warning_cb = warning_cb;
+}
+
+void
+ComboBoxEntryToolItem::set_altx_name(const gchar* altx_name)
+{
+ g_free(_altx_name);
+ _altx_name = g_strdup( altx_name );
+
+ // Widget may not have been created....
+ if(_entry) {
+ g_object_set_data( G_OBJECT(_entry), _altx_name, _entry );
+ }
+}
+
+// Internal ---------------------------------------------------
+
+// Return row of active text or -1 if not found. If exclude is true,
+// use 3d column if available to exclude row from checking (useful to
+// skip rows added for font-families included in doc and not on
+// system)
+gint
+ComboBoxEntryToolItem::get_active_row_from_text(ComboBoxEntryToolItem *action,
+ const gchar *target_text,
+ gboolean exclude,
+ gboolean ignore_case )
+{
+ // Check if text in list
+ gint row = 0;
+ gboolean found = false;
+ GtkTreeIter iter;
+ gboolean valid = gtk_tree_model_get_iter_first( action->_model, &iter );
+ while ( valid ) {
+
+ // See if we should exclude a row
+ gboolean check = true; // If true, font-family is on system.
+ if( exclude && gtk_tree_model_get_n_columns( action->_model ) > 2 ) {
+ gtk_tree_model_get( action->_model, &iter, 2, &check, -1 );
+ }
+
+ if( check ) {
+ // Get text from list entry
+ gchar* text = nullptr;
+ gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); // Column 0
+
+ if( !ignore_case ) {
+ // Case sensitive compare
+ if( strcmp( target_text, text ) == 0 ){
+ found = true;
+ g_free(text);
+ break;
+ }
+ } else {
+ // Case insensitive compare
+ gchar* target_text_casefolded = g_utf8_casefold( target_text, -1 );
+ gchar* text_casefolded = g_utf8_casefold( text, -1 );
+ gboolean equal = (strcmp( target_text_casefolded, text_casefolded ) == 0 );
+ g_free( text_casefolded );
+ g_free( target_text_casefolded );
+ if( equal ) {
+ found = true;
+ g_free(text);
+ break;
+ }
+ }
+ g_free(text);
+ }
+
+ ++row;
+ valid = gtk_tree_model_iter_next( action->_model, &iter );
+ }
+
+ if( !found ) row = -1;
+
+ return row;
+}
+
+// Checks if all comma separated text fragments are in the list and
+// returns a ustring with a list of missing fragments.
+// This is useful for checking if all fonts in a font-family fallback
+// list are available on the system.
+//
+// This routine could also create a Pango Markup string to show which
+// fragments are invalid in the entry box itself. See:
+// http://developer.gnome.org/pango/stable/PangoMarkupFormat.html
+// However... it appears that while one can retrieve the PangoLayout
+// for a GtkEntry box, it is only a copy and changing it has no effect.
+// PangoLayout * pl = gtk_entry_get_layout( entry );
+// pango_layout_set_markup( pl, "NEW STRING", -1 ); // DOESN'T WORK
+Glib::ustring
+ComboBoxEntryToolItem::check_comma_separated_text()
+{
+ Glib::ustring missing;
+
+ // Parse fallback_list using a comma as deliminator
+ gchar** tokens = g_strsplit( _text, ",", 0 );
+
+ gint i = 0;
+ while( tokens[i] != nullptr ) {
+
+ // Remove any surrounding white space.
+ g_strstrip( tokens[i] );
+
+ if( get_active_row_from_text( this, tokens[i], true, true ) == -1 ) {
+ missing += tokens[i];
+ missing += ", ";
+ }
+ ++i;
+ }
+ g_strfreev( tokens );
+
+ // Remove extra comma and space from end.
+ if( missing.size() >= 2 ) {
+ missing.resize( missing.size()-2 );
+ }
+ return missing;
+}
+
+// Callbacks ---------------------------------------------------
+
+void
+ComboBoxEntryToolItem::combo_box_changed_cb( GtkComboBox* widget, gpointer data )
+{
+ // Two things can happen to get here:
+ // An item is selected in the drop-down menu.
+ // Text is typed.
+ // We only react here if an item is selected.
+
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data );
+
+ // Check if item selected:
+ gint newActive = gtk_combo_box_get_active(widget);
+ if( newActive >= 0 && newActive != action->_active ) {
+
+ action->_active = newActive;
+
+ GtkTreeIter iter;
+ if( gtk_combo_box_get_active_iter( GTK_COMBO_BOX( action->_combobox ), &iter ) ) {
+
+ gchar* text = nullptr;
+ gtk_tree_model_get( action->_model, &iter, 0, &text, -1 );
+ gtk_entry_set_text( action->_entry, text );
+
+ g_free( action->_text );
+ action->_text = text;
+ }
+
+ // Now let the world know
+ action->_signal_changed.emit();
+ }
+}
+
+void
+ComboBoxEntryToolItem::entry_activate_cb( GtkEntry *widget,
+ gpointer data )
+{
+ // Get text from entry box.. check if it matches a menu entry.
+
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>( data );
+
+ // Get text
+ g_free( action->_text );
+ action->_text = g_strdup( gtk_entry_get_text( widget ) );
+
+ // Get row
+ action->_active =
+ get_active_row_from_text( action, action->_text );
+
+ // Set active row
+ gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active );
+
+ // Now let the world know
+ action->_signal_changed.emit();
+}
+
+gboolean
+ComboBoxEntryToolItem::match_selected_cb( GtkEntryCompletion* /*widget*/, GtkTreeModel* model, GtkTreeIter* iter, gpointer data )
+{
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data);
+ GtkEntry *entry = action->_entry;
+
+ if( entry) {
+ gchar *family = nullptr;
+ gtk_tree_model_get(model, iter, 0, &family, -1);
+
+ // Set text in GtkEntry
+ gtk_entry_set_text (GTK_ENTRY (entry), family );
+
+ // Set text in ToolItem
+ g_free( action->_text );
+ action->_text = family;
+
+ // Get row
+ action->_active =
+ get_active_row_from_text( action, action->_text );
+
+ // Set active row
+ gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active );
+
+ // Now let the world know
+ action->_signal_changed.emit();
+
+ return true;
+ }
+ return false;
+}
+
+void
+ComboBoxEntryToolItem::defocus()
+{
+ if ( _focusWidget ) {
+ gtk_widget_grab_focus( _focusWidget );
+ }
+}
+
+gboolean
+ComboBoxEntryToolItem::keypress_cb( GtkWidget * /*widget*/, GdkEventKey *event, gpointer data )
+{
+ gboolean wasConsumed = FALSE; /* default to report event not consumed */
+ guint key = 0;
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data);
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ event->hardware_keycode, (GdkModifierType)event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ switch ( key ) {
+
+ // TODO Add bindings for Tab/LeftTab
+ case GDK_KEY_Escape:
+ {
+ //gtk_spin_button_set_value( GTK_SPIN_BUTTON(widget), action->private_data->lastVal );
+ action->defocus();
+ wasConsumed = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ action->defocus();
+ //wasConsumed = TRUE;
+ }
+ break;
+
+
+ }
+
+ return wasConsumed;
+}
+
+}
+}
+}
+
+/*
+ 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/widget/combo-box-entry-tool-item.h b/src/ui/widget/combo-box-entry-tool-item.h
new file mode 100644
index 0000000..3d6440a
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.h
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry.
+ * Features:
+ * Setting GtkEntryBox width in characters.
+ * Passing a function for formatting cells.
+ * Displaying a warning if entry text isn't in list.
+ * Check comma separated values in text against list. (Useful for font-family fallbacks.)
+ * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry)
+ * to allow setting resources.
+ *
+ * Author(s):
+ * Tavmjong Bah
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INK_COMBOBOXENTRY_ACTION
+#define SEEN_INK_COMBOBOXENTRY_ACTION
+
+#include <gtkmm/toolitem.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Creates a Gtk::ToolItem subclass that wraps a Gtk::ComboBox object.
+ */
+class ComboBoxEntryToolItem : public Gtk::ToolItem {
+private:
+ Glib::ustring _tooltip;
+ Glib::ustring _label;
+ GtkTreeModel *_model; ///< Tree Model
+ GtkComboBox *_combobox;
+ GtkEntry *_entry;
+ gint _entry_width;// Width of GtkEntry in characters.
+ gint _extra_width;// Extra Width of GtkComboBox.. to widen drop-down list in list mode.
+ gpointer _cell_data_func; // drop-down menu format
+ gpointer _separator_func;
+ gboolean _popup; // Do we pop-up an entry-completion dialog?
+ GtkEntryCompletion *_entry_completion;
+ GtkWidget *_focusWidget; ///< The widget to return focus to
+
+ GtkWidget *_indicator;
+ gint _active; // Index of active menu item (-1 if not in list).
+ gchar *_text; // Text of active menu item or entry box.
+ gchar *_info; // Text for tooltip info about entry.
+ gpointer _info_cb; // Callback for clicking info icon.
+ gint _info_cb_id;
+ gboolean _info_cb_blocked;
+ gchar *_warning; // Text for tooltip warning that entry isn't in list.
+ gpointer _warning_cb; // Callback for clicking warning icon.
+ gint _warning_cb_id;
+ gboolean _warning_cb_blocked;
+ gchar *_altx_name; // Target for Alt-X keyboard shortcut.
+
+ // Signals
+ sigc::signal<void> _signal_changed;
+
+ void (*changed) (ComboBoxEntryToolItem* action);
+ void (*activated) (ComboBoxEntryToolItem* action);
+
+ static gint get_active_row_from_text(ComboBoxEntryToolItem *action,
+ const gchar *target_text,
+ gboolean exclude = false,
+ gboolean ignore_case = false);
+ void defocus();
+
+ static void combo_box_changed_cb( GtkComboBox* widget, gpointer data );
+ static void entry_activate_cb( GtkEntry *widget,
+ gpointer data );
+ static gboolean match_selected_cb( GtkEntryCompletion *widget,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer data);
+ static gboolean keypress_cb( GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer data );
+
+ Glib::ustring check_comma_separated_text();
+
+public:
+ ComboBoxEntryToolItem(const Glib::ustring name,
+ const Glib::ustring label,
+ const Glib::ustring tooltip,
+ GtkTreeModel *model,
+ gint entry_width = -1,
+ gint extra_width = -1,
+ gpointer cell_data_func = nullptr,
+ gpointer separator_func = nullptr,
+ GtkWidget* focusWidget = nullptr);
+
+ gchar* get_active_text();
+ gboolean set_active_text(const gchar* text, int row=-1);
+
+ void set_entry_width(gint entry_width);
+ void set_extra_width(gint extra_width);
+
+ void popup_enable();
+ void popup_disable();
+ void focus_on_click( bool focus_on_click );
+
+ void set_info( const gchar* info );
+ void set_info_cb( gpointer info_cb );
+ void set_warning( const gchar* warning_cb );
+ void set_warning_cb(gpointer warning );
+ void set_tooltip( const gchar* tooltip );
+
+ void set_altx_name( const gchar* altx_name );
+
+ // Accessor methods
+ decltype(_model) get_model() const {return _model;}
+ decltype(_combobox) get_combobox() const {return _combobox;}
+ decltype(_entry) get_entry() const {return _entry;}
+ decltype(_entry_width) get_entry_width() const {return _entry_width;}
+ decltype(_extra_width) get_extra_width() const {return _extra_width;}
+ decltype(_cell_data_func) get_cell_data_func() const {return _cell_data_func;}
+ decltype(_separator_func) get_separator_func() const {return _separator_func;}
+ decltype(_popup) get_popup() const {return _popup;}
+ decltype(_focusWidget) get_focus_widget() const {return _focusWidget;}
+
+ decltype(_active) get_active() const {return _active;}
+
+ decltype(_signal_changed) signal_changed() {return _signal_changed;}
+
+ // Mutator methods
+ void set_model (decltype(_model) model) {_model = model;}
+ void set_combobox (decltype(_combobox) combobox) {_combobox = combobox;}
+ void set_entry (decltype(_entry) entry) {_entry = entry;}
+ void set_cell_data_func(decltype(_cell_data_func) cell_data_func) {_cell_data_func = cell_data_func;}
+ void set_separator_func(decltype(_separator_func) separator_func) {_separator_func = separator_func;}
+ void set_popup (decltype(_popup) popup) {_popup = popup;}
+ void set_focus_widget (decltype(_focusWidget) focus_widget) {_focusWidget = focus_widget;}
+
+ // This doesn't seem right... surely we should set the active row in the Combobox too?
+ void set_active (decltype(_active) active) {_active = active;}
+};
+
+}
+}
+}
+#endif /* SEEN_INK_COMBOBOXENTRY_ACTION */
+
+/*
+ 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/widget/combo-enums.h b/src/ui/widget/combo-enums.h
new file mode 100644
index 0000000..d574106
--- /dev/null
+++ b/src/ui/widget/combo-enums.h
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+#define INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+
+#include "ui/widget/labelled.h"
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include "attr-widget.h"
+#include "util/enums.h"
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class ComboBoxEnum : public Gtk::ComboBox, public AttrWidget
+{
+private:
+ int on_sort_compare( const Gtk::TreeModel::iterator & a, const Gtk::TreeModel::iterator & b)
+ {
+ Glib::ustring an=(*a)[_columns.label];
+ Glib::ustring bn=(*b)[_columns.label];
+ return an.compare(bn);
+ }
+
+ bool _sort;
+
+public:
+ ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttributeEnum a = SP_ATTR_INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int)default_value), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(int i = 0; i < static_cast<int>(_converter._length); ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active_by_id(default_value);
+
+ // Sort the list
+ if (sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttributeEnum a = SP_ATTR_INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int) 0), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(unsigned int i = 0; i < _converter._length; ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active(0);
+
+ // Sort the list
+ if (_sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_active_data()->key;
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ setProgrammatically = true;
+ const gchar* val = attribute_value(o);
+ if(val)
+ set_active_by_id(_converter.get_id_from_key(val));
+ else
+ set_active(get_default()->as_uint());
+ }
+
+ const Util::EnumData<E>* get_active_data() const
+ {
+ Gtk::TreeModel::iterator i = this->get_active();
+ if(i)
+ return (*i)[_columns.data];
+ return nullptr;
+ }
+
+ void add_row(const Glib::ustring& s)
+ {
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.data] = 0;
+ row[_columns.label] = s;
+ }
+
+ void remove_row(E id) {
+ Gtk::TreeModel::iterator i;
+
+ for(i = _model->children().begin(); i != _model->children().end(); ++i) {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+
+ if(data->id == id)
+ break;
+ }
+
+ if(i != _model->children().end())
+ _model->erase(i);
+ }
+
+ void set_active_by_id(E id) {
+ setProgrammatically = true;
+ for(Gtk::TreeModel::iterator i = _model->children().begin();
+ i != _model->children().end(); ++i)
+ {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+ if(data->id == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ bool on_scroll_event(GdkEventScroll *event) override { return false; }
+
+ void set_active_by_key(const Glib::ustring& key) {
+ setProgrammatically = true;
+ set_active_by_id( _converter.get_id_from_key(key) );
+ };
+
+ bool setProgrammatically;
+
+private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(data);
+ add(label);
+ }
+
+ Gtk::TreeModelColumn<const Util::EnumData<E>*> data;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ const Util::EnumDataConverter<E>& _converter;
+};
+
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class LabelledComboBoxEnum : public Labelled
+{
+public:
+ LabelledComboBoxEnum( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ const Util::EnumDataConverter<E>& c,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true,
+ bool sorted = true)
+ : Labelled(label, tooltip, new ComboBoxEnum<E>(c, SP_ATTR_INVALID, sorted), suffix, icon, mnemonic)
+ {
+ }
+
+ ComboBoxEnum<E>* getCombobox() {
+ return static_cast< ComboBoxEnum<E>* > (_widget);
+ }
+};
+
+}
+}
+}
+
+#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/widget/combo-tool-item.cpp b/src/ui/widget/combo-tool-item.cpp
new file mode 100644
index 0000000..ffc7e75
--- /dev/null
+++ b/src/ui/widget/combo-tool-item.cpp
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+/** \file
+ A combobox that can be displayed in a toolbar.
+*/
+
+#include "combo-tool-item.h"
+#include "preferences.h"
+#include <iostream>
+#include <utility>
+#include <gtkmm/toolitem.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ComboToolItem*
+ComboToolItem::create(const Glib::ustring &group_label,
+ const Glib::ustring &tooltip,
+ const Glib::ustring &stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry)
+{
+ return new ComboToolItem(group_label, tooltip, stock_id, store, has_entry);
+}
+
+ComboToolItem::ComboToolItem(Glib::ustring group_label,
+ Glib::ustring tooltip,
+ Glib::ustring stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry) :
+ _active(-1),
+ _group_label(std::move( group_label )),
+ _tooltip(std::move( tooltip )),
+ _stock_id(std::move( stock_id )),
+ _store (std::move(store)),
+ _use_label (true),
+ _use_icon (false),
+ _use_pixbuf (true),
+ _icon_size ( Gtk::ICON_SIZE_LARGE_TOOLBAR ),
+ _combobox (nullptr),
+ _group_label_widget(nullptr),
+ _container(Gtk::manage(new Gtk::Box())),
+ _menuitem (nullptr)
+{
+ add(*_container);
+ _container->set_spacing(3);
+
+ // ": " is added to the group label later
+ if (!_group_label.empty()) {
+ // we don't expect trailing spaces
+ // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ' ');
+
+ // strip space (note: raw() indexing is much cheaper on Glib::ustring)
+ if (_group_label.raw()[_group_label.raw().size() - 1] == ' ') {
+ _group_label.resize(_group_label.size() - 1);
+ }
+ }
+ if (!_group_label.empty()) {
+ // we don't expect a trailing colon
+ // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ':');
+
+ // strip colon (note: raw() indexing is much cheaper on Glib::ustring)
+ if (_group_label.raw()[_group_label.raw().size() - 1] == ':') {
+ _group_label.resize(_group_label.size() - 1);
+ }
+ }
+
+
+ // Create combobox
+ _combobox = Gtk::manage (new Gtk::ComboBox(has_entry));
+ _combobox->set_model(_store);
+
+ populate_combobox();
+
+ _combobox->signal_changed().connect(
+ sigc::mem_fun(*this, &ComboToolItem::on_changed_combobox));
+ _container->pack_start(*_combobox);
+
+ show_all();
+}
+
+void
+ComboToolItem::focus_on_click( bool focus_on_click )
+{
+ _combobox->set_focus_on_click(focus_on_click);
+}
+
+
+void
+ComboToolItem::use_label(bool use_label)
+{
+ _use_label = use_label;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_icon(bool use_icon)
+{
+ _use_icon = use_icon;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_pixbuf(bool use_pixbuf)
+{
+ _use_pixbuf = use_pixbuf;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_group_label(bool use_group_label)
+{
+ if (use_group_label == (_group_label_widget != nullptr)) {
+ return;
+ }
+ if (use_group_label) {
+ _container->remove(*_combobox);
+ _group_label_widget = Gtk::manage(new Gtk::Label(_group_label + ": "));
+ _container->pack_start(*_group_label_widget);
+ _container->pack_start(*_combobox);
+ } else {
+ _container->remove(*_group_label_widget);
+ delete _group_label_widget;
+ _group_label_widget = nullptr;
+ }
+}
+
+void
+ComboToolItem::populate_combobox()
+{
+ _combobox->clear();
+
+ ComboToolItemColumns columns;
+ if (_use_icon) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ auto children = _store->children();
+ for (auto row : children) {
+ Glib::ustring icon = row[columns.col_icon];
+ gint pos = icon.find("-symbolic");
+ if (pos == std::string::npos) {
+ icon += "-symbolic";
+ }
+ row[columns.col_icon] = icon;
+ }
+ }
+ Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf;
+ renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _combobox->pack_start (*Gtk::manage(renderer), false);
+ _combobox->add_attribute (*renderer, "icon_name", columns.col_icon );
+ } else if (_use_pixbuf) {
+ Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf;
+ //renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _combobox->pack_start (*Gtk::manage(renderer), false);
+ _combobox->add_attribute (*renderer, "pixbuf", columns.col_pixbuf );
+ }
+
+ if (_use_label) {
+ _combobox->pack_start(columns.col_label);
+ }
+
+ std::vector<Gtk::CellRenderer*> cells = _combobox->get_cells();
+ for (auto & cell : cells) {
+ _combobox->add_attribute (*cell, "sensitive", columns.col_sensitive);
+ }
+
+ set_tooltip_text(_tooltip);
+ _combobox->set_tooltip_text(_tooltip);
+ _combobox->set_active (_active);
+}
+
+void
+ComboToolItem::set_active (gint active) {
+ if (_active != active) {
+
+ _active = active;
+
+ if (_combobox) {
+ _combobox->set_active (active);
+ }
+
+ if (active < _radiomenuitems.size()) {
+ _radiomenuitems[ active ]->set_active();
+ }
+ }
+}
+
+Glib::ustring
+ComboToolItem::get_active_text () {
+ Gtk::TreeModel::Row row = _store->children()[_active];
+ ComboToolItemColumns columns;
+ Glib::ustring label = row[columns.col_label];
+ return label;
+}
+
+bool
+ComboToolItem::on_create_menu_proxy()
+{
+ if (_menuitem == nullptr) {
+
+ _menuitem = Gtk::manage (new Gtk::MenuItem(_group_label));
+ Gtk::Menu *menu = Gtk::manage (new Gtk::Menu);
+
+ Gtk::RadioButton::Group group;
+ int index = 0;
+ auto children = _store->children();
+ for (auto row : children) {
+ ComboToolItemColumns columns;
+ Glib::ustring label = row[columns.col_label ];
+ Glib::ustring icon = row[columns.col_icon ];
+ Glib::ustring tooltip = row[columns.col_tooltip ];
+ bool sensitive = row[columns.col_sensitive ];
+
+ Gtk::RadioMenuItem* button = Gtk::manage(new Gtk::RadioMenuItem(group));
+ button->set_label (label);
+ button->set_tooltip_text( tooltip );
+ button->set_sensitive( sensitive );
+
+ button->signal_toggled().connect( sigc::bind<0>(
+ sigc::mem_fun(*this, &ComboToolItem::on_toggled_radiomenu), index++)
+ );
+
+ menu->add (*button);
+
+ _radiomenuitems.push_back( button );
+ }
+
+ if ( _active < _radiomenuitems.size()) {
+ _radiomenuitems[ _active ]->set_active();
+ }
+
+ _menuitem->set_submenu (*menu);
+ _menuitem->show_all();
+ }
+
+ set_proxy_menu_item(_group_label, *_menuitem);
+ return true;
+}
+
+void
+ComboToolItem::on_changed_combobox() {
+
+ int row = _combobox->get_active_row_number();
+ set_active( row );
+ _changed.emit (_active);
+ _changed_after.emit (_active);
+}
+
+void
+ComboToolItem::on_toggled_radiomenu(int n) {
+
+ // toggled emitted twice, first for button toggled off, second for button toggled on.
+ // We want to react only to the button turned on.
+ if ( n < _radiomenuitems.size() &&_radiomenuitems[ n ]->get_active()) {
+ set_active ( n );
+ _changed.emit (_active);
+ _changed_after.emit (_active);
+ }
+}
+
+}
+}
+}
+/*
+ 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/widget/combo-tool-item.h b/src/ui/widget/combo-tool-item.h
new file mode 100644
index 0000000..1fc8b00
--- /dev/null
+++ b/src/ui/widget/combo-tool-item.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_COMBO_TOOL_ITEM
+#define SEEN_COMBO_TOOL_ITEM
+
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/**
+ A combobox that can be displayed in a toolbar
+*/
+
+#include <gtkmm/toolitem.h>
+#include <gtkmm/liststore.h>
+#include <sigc++/sigc++.h>
+#include <vector>
+
+namespace Gtk {
+class Box;
+class ComboBox;
+class Label;
+class MenuItem;
+class RadioMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class ComboToolItemColumns : public Gtk::TreeModel::ColumnRecord {
+public:
+ ComboToolItemColumns() {
+ add (col_label);
+ add (col_value);
+ add (col_icon);
+ add (col_pixbuf);
+ add (col_data); // Used to store a pointer
+ add (col_tooltip);
+ add (col_sensitive);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_label;
+ Gtk::TreeModelColumn<Glib::ustring> col_value;
+ Gtk::TreeModelColumn<Glib::ustring> col_icon;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > col_pixbuf;
+ Gtk::TreeModelColumn<void *> col_data;
+ Gtk::TreeModelColumn<Glib::ustring> col_tooltip;
+ Gtk::TreeModelColumn<bool> col_sensitive;
+};
+
+
+class ComboToolItem : public Gtk::ToolItem {
+
+public:
+ static ComboToolItem* create(const Glib::ustring &label,
+ const Glib::ustring &tooltip,
+ const Glib::ustring &stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+
+ /* Style of combobox */
+ void use_label( bool use_label );
+ void use_icon( bool use_icon );
+ void focus_on_click( bool focus_on_click );
+ void use_pixbuf( bool use_pixbuf );
+ void use_group_label( bool use_group_label ); // Applies to tool item only
+
+ gint get_active() { return _active; }
+ Glib::ustring get_active_text();
+ void set_active( gint active );
+ void set_icon_size( Gtk::BuiltinIconSize size ) { _icon_size = size; }
+
+ Glib::RefPtr<Gtk::ListStore> get_store() { return _store; }
+
+ sigc::signal<void, int> signal_changed() { return _changed; }
+ sigc::signal<void, int> signal_changed_after() { return _changed_after; }
+
+protected:
+ bool on_create_menu_proxy() override;
+ void populate_combobox();
+
+ /* Signals */
+ sigc::signal<void, int> _changed;
+ sigc::signal<void, int> _changed_after; // Needed for unit tracker which eats _changed.
+
+private:
+
+ Glib::ustring _group_label;
+ Glib::ustring _tooltip;
+ Glib::ustring _stock_id;
+ Glib::RefPtr<Gtk::ListStore> _store;
+
+ gint _active; /* Active menu item/button */
+
+ /* Style */
+ bool _use_label;
+ bool _use_icon; // Applies to menu item only
+ bool _use_pixbuf;
+ Gtk::BuiltinIconSize _icon_size;
+
+ /* Combobox in tool */
+ Gtk::ComboBox* _combobox;
+ Gtk::Label* _group_label_widget;
+ Gtk::Box* _container;
+
+ Gtk::MenuItem* _menuitem;
+ std::vector<Gtk::RadioMenuItem*> _radiomenuitems;
+
+ /* Internal Callbacks */
+ void on_changed_combobox();
+ void on_toggled_radiomenu(int n);
+
+ ComboToolItem(Glib::ustring group_label,
+ Glib::ustring tooltip,
+ Glib::ustring stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+};
+}
+}
+}
+#endif /* SEEN_COMBO_TOOL_ITEM */
+
+/*
+ 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/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp
new file mode 100644
index 0000000..897b964
--- /dev/null
+++ b/src/ui/widget/dash-selector.cpp
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Combobox for selecting dash patterns - implementation.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dash-selector.h"
+
+#include <cstring>
+
+#include <glibmm/i18n.h>
+
+#include <2geom/coord.h>
+
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "style.h"
+
+#include "ui/dialog-events.h"
+#include "ui/widget/spinbutton.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+gchar const *const DashSelector::_prefs_path = "/palette/dashes";
+
+static double dash_0[] = {-1.0};
+static double dash_1_1[] = {1.0, 1.0, -1.0};
+static double dash_2_1[] = {2.0, 1.0, -1.0};
+static double dash_4_1[] = {4.0, 1.0, -1.0};
+static double dash_1_2[] = {1.0, 2.0, -1.0};
+static double dash_1_4[] = {1.0, 4.0, -1.0};
+
+static size_t BD_LEN = 7; // must correspond to the number of entries in the next line
+static double *builtin_dashes[] = {dash_0, dash_1_1, dash_2_1, dash_4_1, dash_1_2, dash_1_4, nullptr};
+
+static double **dashes = nullptr;
+
+DashSelector::DashSelector()
+ : preview_width(80),
+ preview_height(16),
+ preview_lineheight(2)
+{
+ set_spacing(4);
+
+ // TODO: find something more sensible here!!
+ init_dashes();
+
+ dash_store = Gtk::ListStore::create(dash_columns);
+ dash_combo.set_model(dash_store);
+ dash_combo.pack_start(image_renderer);
+ dash_combo.set_cell_data_func(image_renderer, sigc::mem_fun(*this, &DashSelector::prepareImageRenderer));
+ dash_combo.set_tooltip_text(_("Dash pattern"));
+ dash_combo.get_style_context()->add_class("combobright");
+ dash_combo.show();
+ dash_combo.signal_changed().connect( sigc::mem_fun(*this, &DashSelector::on_selection) );
+
+ this->pack_start(dash_combo, true, true, 0);
+
+ offset = Gtk::Adjustment::create(0.0, 0.0, 10.0, 0.1, 1.0, 0.0);
+ offset->signal_value_changed().connect(sigc::mem_fun(*this, &DashSelector::offset_value_changed));
+ auto sb = new Inkscape::UI::Widget::SpinButton(offset, 0.1, 2);
+ sb->set_tooltip_text(_("Pattern offset"));
+ sp_dialog_defocus_on_enter_cpp(sb);
+ sb->show();
+
+ this->pack_start(*sb, false, false, 0);
+
+ int np=0;
+ while (dashes[np]){ np++;}
+ for (int i = 0; i<np-1; i++) { // all but the custom one go this way
+ // Add the dashes to the combobox
+ Gtk::TreeModel::Row row = *(dash_store->append());
+ row[dash_columns.dash] = dashes[i];
+ row[dash_columns.pixbuf] = Glib::wrap(sp_dash_to_pixbuf(dashes[i]));
+ }
+ // add the custom one
+ Gtk::TreeModel::Row row = *(dash_store->append());
+ row[dash_columns.dash] = dashes[np-1];
+ row[dash_columns.pixbuf] = Glib::wrap(sp_text_to_pixbuf((char *)"Custom"));
+
+ this->set_data("pattern", dashes[0]);
+}
+
+DashSelector::~DashSelector() {
+ // FIXME: for some reason this doesn't get called; does the call to manage() in
+ // sp_stroke_style_line_widget_new() not processed correctly?
+}
+
+void DashSelector::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) {
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = (*row)[dash_columns.pixbuf];
+ image_renderer.property_pixbuf() = pixbuf;
+}
+
+void DashSelector::init_dashes() {
+
+ if (!dashes) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::vector<Glib::ustring> dash_prefs = prefs->getAllDirs(_prefs_path);
+
+ int pos = 0;
+ if (!dash_prefs.empty()) {
+ SPStyle style;
+ dashes = g_new (double *, dash_prefs.size() + 2); // +1 for custom slot, +1 for terminator slot
+
+ for (auto & dash_pref : dash_prefs) {
+ style.readFromPrefs( dash_pref );
+
+ if (!style.stroke_dasharray.values.empty()) {
+ dashes[pos] = g_new (double, style.stroke_dasharray.values.size() + 1);
+ double *d = dashes[pos];
+ unsigned i = 0;
+ for (; i < style.stroke_dasharray.values.size(); i++) {
+ d[i] = style.stroke_dasharray.values[i].value;
+ }
+ d[i] = -1;
+ } else {
+ dashes[pos] = dash_0;
+ }
+ pos += 1;
+ }
+ } else { // This code may never execute - a new preferences.xml is created for a new user. Maybe if the user deletes dashes from preferences.xml?
+ dashes = g_new (double *, BD_LEN + 2); // +1 for custom slot, +1 for terminator slot
+ unsigned i;
+ for(i=0;i<BD_LEN;i++) {
+ dashes[i] = builtin_dashes[i];
+ }
+ pos = BD_LEN;
+ }
+ // make a place to hold the custom dashes, up to 15 positions long (+ terminator)
+ dashes[pos] = g_new (double, 16);
+ double *d = dashes[pos];
+ int i=0;
+ for(i=0;i<15;i++){ d[i]=i; } // have to put something in there, this is a pattern hopefully nobody would choose
+ d[15]=-1.0;
+ // final terminator
+ dashes[++pos] = nullptr;
+ }
+}
+
+void DashSelector::set_dash (int ndash, double *dash, double o)
+{
+ int pos = -1; // Allows custom patterns to remain unscathed by this.
+ int count = 0; // will hold the NULL terminator at the end of the dashes list
+ if (ndash > 0) {
+ double delta = 0.0;
+ for (int i = 0; i < ndash; i++)
+ delta += dash[i];
+ delta /= 1000.0;
+
+ for (int i = 0; dashes[i]; i++,count++) {
+ double *pattern = dashes[i];
+ int np = 0;
+ while (pattern[np] >= 0.0)
+ np += 1;
+ if (np == ndash) {
+ int j;
+ for (j = 0; j < ndash; j++) {
+ if (!Geom::are_near(dash[j], pattern[j], delta)) {
+ break;
+ }
+ }
+ if (j == ndash) {
+ pos = i;
+ break;
+ }
+ }
+ }
+ }
+ else if(ndash==0) {
+ pos = 0;
+ }
+ if(pos>=0){
+ this->set_data("pattern", dashes[pos]);
+ this->dash_combo.set_active(pos);
+ this->offset->set_value(o);
+ if(pos == 10) {
+ this->offset->set_value(10.0);
+ }
+ }
+ else { // Hit a custom pattern in the SVG, write it into the combobox.
+ count--; // the one slot for custom patterns
+ double *d = dashes[count];
+ int i=0;
+ for(i=0;i< (ndash > 15 ? 15 : ndash) ;i++) {
+ d[i]=dash[i];
+ } // store the custom pattern
+ d[ndash]=-1.0; //terminate it
+ this->set_data("pattern", dashes[count]);
+ this->dash_combo.set_active(count);
+ this->offset->set_value(o); // what does this do????
+ }
+}
+
+void DashSelector::get_dash(int *ndash, double **dash, double *off)
+{
+ double *pattern = (double*) this->get_data("pattern");
+
+ int nd = 0;
+ while (pattern[nd] >= 0.0)
+ nd += 1;
+
+ if (nd > 0) {
+ if (ndash)
+ *ndash = nd;
+ if (dash) {
+ *dash = g_new (double, nd);
+ memcpy (*dash, pattern, nd * sizeof (double));
+ }
+ if (off)
+ *off = offset->get_value();
+ } else {
+ if (ndash)
+ *ndash = 0;
+ if (dash)
+ *dash = nullptr;
+ if (off)
+ *off = 0.0;
+ }
+}
+
+/**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+GdkPixbuf* DashSelector::sp_dash_to_pixbuf(double *pattern)
+{
+ int n_dashes;
+ for (n_dashes = 0; pattern[n_dashes] >= 0.0; n_dashes ++) ;
+
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, preview_width, preview_height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_set_line_width (ct, preview_lineheight);
+ cairo_scale (ct, preview_lineheight, 1);
+ //cairo_set_source_rgb (ct, 0, 0, 0);
+ cairo_move_to (ct, 0, preview_height/2);
+ cairo_line_to (ct, preview_width, preview_height/2);
+ cairo_set_dash(ct, pattern, n_dashes, 0);
+ cairo_stroke (ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+/**
+ * Fill a pixbuf with a text label using standard cairo drawing
+ */
+GdkPixbuf* DashSelector::sp_text_to_pixbuf(char *text)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, preview_width, preview_height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_select_font_face (ct, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
+ cairo_set_font_size (ct, 12.0);
+ cairo_set_source_rgb (ct, 0.0, 0.0, 0.0);
+ cairo_move_to (ct, 16.0, 13.0);
+ cairo_show_text (ct, text);
+
+ cairo_stroke (ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+void DashSelector::on_selection ()
+{
+ double *pattern = dash_combo.get_active()->get_value(dash_columns.dash);
+ this->set_data ("pattern", pattern);
+
+ changed_signal.emit();
+}
+
+void DashSelector::offset_value_changed()
+{
+ changed_signal.emit();
+}
+} // namespace Widget
+} // 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/widget/dash-selector.h b/src/ui/widget/dash-selector.h
new file mode 100644
index 0000000..449392a
--- /dev/null
+++ b/src/ui/widget/dash-selector.h
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_DASH_SELECTOR_NEW_H
+#define SEEN_SP_DASH_SELECTOR_NEW_H
+
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Maximilian Albert <maximilian.albert> (gtkmm-ification)
+ *
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Class that wraps a combobox and spinbutton for selecting dash patterns.
+ */
+class DashSelector : public Gtk::HBox {
+public:
+ DashSelector();
+ ~DashSelector() override;
+
+ /**
+ * Get and set methods for dashes
+ */
+ void set_dash(int ndash, double *dash, double offset);
+ void get_dash(int *ndash, double **dash, double *offset);
+
+ sigc::signal<void> changed_signal;
+
+private:
+
+ /**
+ * Initialize dashes list from preferences
+ */
+ static void init_dashes();
+
+ /**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+ GdkPixbuf* sp_dash_to_pixbuf(double *pattern);
+
+ /**
+ * Fill a pixbuf with text standard cairo drawing
+ */
+ GdkPixbuf* sp_text_to_pixbuf(char *text);
+
+ /**
+ * Callback for combobox image renderer
+ */
+ void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row );
+
+ /**
+ * Callback for offset adjustment changing
+ */
+ void offset_value_changed();
+
+ /**
+ * Callback for combobox selection changing
+ */
+ void on_selection();
+
+ /**
+ * Combobox columns
+ */
+ class DashColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<double *> dash;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > pixbuf;
+
+ DashColumns() {
+ add(dash); add(pixbuf);
+ }
+ };
+ DashColumns dash_columns;
+ Glib::RefPtr<Gtk::ListStore> dash_store;
+ Gtk::ComboBox dash_combo;
+ Gtk::CellRendererPixbuf image_renderer;
+ Glib::RefPtr<Gtk::Adjustment> offset;
+
+ static gchar const *const _prefs_path;
+ int preview_width;
+ int preview_height;
+ int preview_lineheight;
+
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SP_DASH_SELECTOR_NEW_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/widget/dock-item.cpp b/src/ui/widget/dock-item.cpp
new file mode 100644
index 0000000..01786d5
--- /dev/null
+++ b/src/ui/widget/dock-item.cpp
@@ -0,0 +1,528 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/dock.h"
+
+#include "desktop.h"
+#include "inkscape.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include <glibmm/exceptionhandler.h>
+#include <gtkmm/icontheme.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+DockItem::DockItem(Dock& dock, const Glib::ustring& name, const Glib::ustring& long_name,
+ const Glib::ustring& icon_name, State state, GdlDockPlacement placement) :
+ _dock(dock),
+ _prev_state(state),
+ _prev_position(0),
+ _window(nullptr),
+ _x(0),
+ _y(0),
+ _grab_focus_on_realize(false),
+ _gdl_dock_item(nullptr)
+{
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ GdlDockItemBehavior gdl_dock_behavior =
+ (prefs->getBool("/options/dock/cancenterdock", true) ?
+ GDL_DOCK_ITEM_BEH_NORMAL :
+ GDL_DOCK_ITEM_BEH_CANT_DOCK_CENTER);
+
+
+ if (!icon_name.empty()) {
+ _icon_pixbuf = sp_get_icon_pixbuf(icon_name, "/toolbox/secondary");
+ }
+
+ if ( _icon_pixbuf ) {
+ _gdl_dock_item = gdl_dock_item_new_with_pixbuf_icon( name.c_str(), long_name.c_str(),
+ _icon_pixbuf->gobj(), gdl_dock_behavior );
+ } else {
+ _gdl_dock_item = gdl_dock_item_new(name.c_str(), long_name.c_str(), gdl_dock_behavior);
+ }
+
+ _frame.set_shadow_type(Gtk::SHADOW_IN);
+ gtk_container_add (GTK_CONTAINER (_gdl_dock_item), GTK_WIDGET (_frame.gobj()));
+ _frame.add(_dock_item_box);
+ _dock_item_box.set_border_width(3);
+
+ signal_drag_begin().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDragBegin));
+ signal_drag_end().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDragEnd));
+ signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onHide), false);
+ signal_show().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onShow), false);
+ signal_state_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onStateChanged));
+ signal_delete_event().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDeleteEvent));
+ signal_realize().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onRealize));
+
+ _dock.addItem(*this, ( _prev_state == FLOATING_STATE || _prev_state == ICONIFIED_FLOATING_STATE ) ? GDL_DOCK_FLOATING : placement);
+
+ if (_prev_state == ICONIFIED_FLOATING_STATE || _prev_state == ICONIFIED_DOCKED_STATE) {
+ iconify();
+ }
+
+ show_all();
+
+}
+
+DockItem::~DockItem()
+{
+ g_free(_gdl_dock_item);
+}
+
+Gtk::Widget&
+DockItem::getWidget()
+{
+ return *Glib::wrap(GTK_WIDGET(_gdl_dock_item));
+}
+
+GtkWidget *
+DockItem::gobj()
+{
+ return _gdl_dock_item;
+}
+
+Gtk::VBox *
+DockItem::get_vbox()
+{
+ return &_dock_item_box;
+}
+
+
+void
+DockItem::get_position(int& x, int& y)
+{
+ if (getWindow()) {
+ getWindow()->get_position(x, y);
+ } else {
+ x = _x;
+ y = _y;
+ }
+}
+
+void
+DockItem::get_size(int& width, int& height)
+{
+ if (getWindow()) {
+ getWindow()->get_size(width, height);
+ } else {
+ width = get_vbox()->get_width();
+ height = get_vbox()->get_height();
+ }
+}
+
+
+void
+DockItem::resize(int width, int height)
+{
+ if (_window)
+ _window->resize(width, height);
+}
+
+
+void
+DockItem::move(int x, int y)
+{
+ if (_window)
+ _window->move(x, y);
+}
+
+void
+DockItem::set_position(Gtk::WindowPosition position)
+{
+ if (_window)
+ _window->set_position(position);
+}
+
+void
+DockItem::set_size_request(int width, int height)
+{
+ getWidget().set_size_request(width, height);
+}
+
+void DockItem::size_request(Gtk::Requisition& requisition)
+{
+ Gtk::Requisition req_natural;
+ getWidget().get_preferred_size(req_natural, requisition);
+}
+
+void
+DockItem::set_title(Glib::ustring title)
+{
+ g_object_set (_gdl_dock_item,
+ "long-name", title.c_str(),
+ NULL);
+
+ gdl_dock_item_set_tablabel(GDL_DOCK_ITEM(_gdl_dock_item),
+ gtk_label_new (title.c_str()));
+}
+
+bool
+DockItem::isAttached() const
+{
+ return GDL_DOCK_OBJECT_ATTACHED (_gdl_dock_item);
+}
+
+
+bool
+DockItem::isFloating() const
+{
+ return (GTK_WIDGET(gdl_dock_object_get_toplevel(GDL_DOCK_OBJECT (_gdl_dock_item))) !=
+ _dock.getGdlWidget());
+}
+
+bool
+DockItem::isIconified() const
+{
+ return GDL_DOCK_ITEM_ICONIFIED (_gdl_dock_item);
+}
+
+DockItem::State
+DockItem::getState() const
+{
+ if (isIconified() && _prev_state == FLOATING_STATE) {
+ return ICONIFIED_FLOATING_STATE;
+ } else if (isIconified()) {
+ return ICONIFIED_DOCKED_STATE;
+ } else if (isFloating() && isAttached()) {
+ return FLOATING_STATE;
+ } else if (isAttached()) {
+ return DOCKED_STATE;
+ }
+
+ return UNATTACHED;
+}
+
+DockItem::State
+DockItem::getPrevState() const
+{
+ return _prev_state;
+}
+
+GdlDockPlacement
+DockItem::getPlacement() const
+{
+ GdlDockPlacement placement = GDL_DOCK_TOP;
+ GdlDockObject *parent = gdl_dock_object_get_parent_object (GDL_DOCK_OBJECT(_gdl_dock_item));
+ if (parent) {
+ gdl_dock_object_child_placement(parent, GDL_DOCK_OBJECT(_gdl_dock_item), &placement);
+ }
+
+ return placement;
+}
+
+void
+DockItem::hide()
+{
+ gdl_dock_item_hide_item (GDL_DOCK_ITEM(_gdl_dock_item));
+}
+
+void
+DockItem::show()
+{
+ gdl_dock_item_show_item (GDL_DOCK_ITEM(_gdl_dock_item));
+}
+
+void
+DockItem::iconify()
+{
+ gdl_dock_item_iconify_item (GDL_DOCK_ITEM(_gdl_dock_item));
+}
+
+void
+DockItem::show_all()
+{
+ gtk_widget_show_all(_gdl_dock_item);
+}
+
+void
+DockItem::present()
+{
+ gdl_dock_object_present(GDL_DOCK_OBJECT(_gdl_dock_item), nullptr);
+
+ // always grab focus, even if we're already present
+ grab_focus();
+
+ if (!isFloating() && getWidget().get_realized())
+ _dock.scrollToItem(*this);
+}
+
+
+void
+DockItem::grab_focus()
+{
+ if (gtk_widget_get_realized (_gdl_dock_item)) {
+
+ // make sure the window we're in is present
+ Gtk::Widget *toplevel = getWidget().get_toplevel();
+ if (Gtk::Window *window = dynamic_cast<Gtk::Window *>(toplevel)) {
+ window->present();
+ }
+
+ gtk_widget_grab_focus (_gdl_dock_item);
+
+ } else {
+ _grab_focus_on_realize = true;
+ }
+}
+
+
+/* Signal wrappers */
+
+Glib::SignalProxy0<void>
+DockItem::signal_show()
+{
+ return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_show_proxy);
+}
+
+Glib::SignalProxy0<void>
+DockItem::signal_hide()
+{
+ return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_hide_proxy);
+}
+
+Glib::SignalProxy1<bool, GdkEventAny *>
+DockItem::signal_delete_event()
+{
+ return Glib::SignalProxy1<bool, GdkEventAny *>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_delete_event_proxy);
+}
+
+Glib::SignalProxy0<void>
+DockItem::signal_drag_begin()
+{
+ return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_drag_begin_proxy);
+}
+
+Glib::SignalProxy1<void, bool>
+DockItem::signal_drag_end()
+{
+ return Glib::SignalProxy1<void, bool>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_drag_end_proxy);
+}
+
+Glib::SignalProxy0<void>
+DockItem::signal_realize()
+{
+ return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)),
+ &_signal_realize_proxy);
+}
+
+sigc::signal<void, DockItem::State, DockItem::State>
+DockItem::signal_state_changed()
+{
+ return _signal_state_changed;
+}
+
+void
+DockItem::_onHideWindow()
+{
+ if (_window)
+ _window->get_position(_x, _y);
+}
+
+void
+DockItem::_onHide()
+{
+ if (_prev_state == ICONIFIED_DOCKED_STATE)
+ _prev_state = DOCKED_STATE;
+ else if (_prev_state == ICONIFIED_FLOATING_STATE)
+ _prev_state = FLOATING_STATE;
+
+ _signal_state_changed.emit(UNATTACHED, getState());
+}
+
+void
+DockItem::_onShow()
+{
+ _signal_state_changed.emit(UNATTACHED, getState());
+}
+
+void
+DockItem::_onDragBegin()
+{
+ _prev_state = getState();
+ if (_prev_state == FLOATING_STATE)
+ _dock.toggleDockable(getWidget().get_width(), getWidget().get_height());
+}
+
+void
+DockItem::_onDragEnd(bool)
+{
+ State state = getState();
+
+ if (state != _prev_state)
+ _signal_state_changed.emit(_prev_state, state);
+
+ if (state == FLOATING_STATE) {
+ if (_prev_state == FLOATING_STATE)
+ _dock.toggleDockable();
+ }
+
+ _prev_state = state;
+}
+
+void
+DockItem::_onRealize()
+{
+ if (_grab_focus_on_realize) {
+ _grab_focus_on_realize = false;
+ grab_focus();
+ }
+}
+
+bool
+DockItem::_onKeyPress(GdkEventKey *event)
+{
+ gboolean return_value;
+ g_signal_emit_by_name (_gdl_dock_item, "key_press_event", event, &return_value);
+ return return_value;
+}
+
+void
+DockItem::_onStateChanged(State /*prev_state*/, State new_state)
+{
+ _window = getWindow();
+ if(_window)
+ _window->set_type_hint(Gdk::WINDOW_TYPE_HINT_NORMAL);
+
+ if (new_state == FLOATING_STATE && _window) {
+ _window->signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onHideWindow));
+ _signal_key_press_event_connection =
+ _window->signal_key_press_event().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onKeyPress));
+ }
+}
+
+
+bool
+DockItem::_onDeleteEvent(GdkEventAny */*event*/)
+{
+ hide();
+ return false;
+}
+
+
+Gtk::Window *
+DockItem::getWindow()
+{
+ g_return_val_if_fail(_gdl_dock_item, 0);
+ Gtk::Container *parent = getWidget().get_parent();
+ parent = (parent ? parent->get_parent() : nullptr);
+ return (parent ? dynamic_cast<Gtk::Window *>(parent) : nullptr);
+}
+
+const Glib::SignalProxyInfo
+DockItem::_signal_show_proxy =
+{
+ "show",
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback,
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback
+};
+
+const Glib::SignalProxyInfo
+DockItem::_signal_hide_proxy =
+{
+ "hide",
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback,
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback
+};
+
+
+const Glib::SignalProxyInfo
+DockItem::_signal_delete_event_proxy =
+{
+ "delete_event",
+ (GCallback) &_signal_delete_event_callback,
+ (GCallback) &_signal_delete_event_callback
+};
+
+
+const Glib::SignalProxyInfo
+DockItem::_signal_drag_begin_proxy =
+{
+ "dock-drag-begin",
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback,
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback
+};
+
+
+const Glib::SignalProxyInfo
+DockItem::_signal_drag_end_proxy =
+{
+ "dock_drag_end",
+ (GCallback) &_signal_drag_end_callback,
+ (GCallback) &_signal_drag_end_callback
+};
+
+
+const Glib::SignalProxyInfo
+DockItem::_signal_realize_proxy =
+{
+ "realize",
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback,
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback
+};
+
+
+gboolean
+DockItem::_signal_delete_event_callback(GtkWidget *self, GdkEventAny *event, void *data)
+{
+ using namespace Gtk;
+ typedef sigc::slot<bool, GdkEventAny *> SlotType;
+
+ if (Glib::ObjectBase::_get_current_wrapper((GObject *) self)) {
+ try {
+ if(sigc::slot_base *const slot = Glib::SignalProxyNormal::data_to_slot(data))
+ return static_cast<int>( (*static_cast<SlotType*>(slot))(event) );
+ } catch(...) {
+ Glib::exception_handlers_invoke();
+ }
+ }
+
+ typedef gboolean RType;
+ return RType();
+}
+
+void
+DockItem::_signal_drag_end_callback(GtkWidget *self, gboolean cancelled, void *data)
+{
+ using namespace Gtk;
+ typedef sigc::slot<void, bool> SlotType;
+
+ if (Glib::ObjectBase::_get_current_wrapper((GObject *) self)) {
+ try {
+ if(sigc::slot_base *const slot = Glib::SignalProxyNormal::data_to_slot(data))
+ (*static_cast<SlotType *>(slot))(cancelled);
+ } catch(...) {
+ Glib::exception_handlers_invoke();
+ }
+ }
+}
+
+
+} // namespace Widget
+} // 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/widget/dock-item.h b/src/ui/widget/dock-item.h
new file mode 100644
index 0000000..be7ac77
--- /dev/null
+++ b/src/ui/widget/dock-item.h
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#ifndef INKSCAPE_UI_WIGET_DOCK_ITEM_H
+#define INKSCAPE_UI_WIGET_DOCK_ITEM_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/window.h>
+
+#include <gdl/gdl.h>
+
+namespace Gtk {
+ class HButtonBox;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Dock;
+
+/**
+ * A custom wrapper around gdl-dock-item.
+ */
+class DockItem {
+
+public:
+
+ enum State { UNATTACHED, // item not bound to the dock (a temporary state)
+ FLOATING_STATE, // item not in its dock (but can be docked in other,
+ // e.g. floating, docks)
+ DOCKED_STATE, // item in its assigned dock
+ ICONIFIED_DOCKED_STATE, // item iconified in its assigned dock from dock
+ ICONIFIED_FLOATING_STATE}; // item iconified in its assigned dock from float
+
+ DockItem(Dock& dock, const Glib::ustring& name, const Glib::ustring& long_name,
+ const Glib::ustring& icon_name, State state, GdlDockPlacement placement);
+
+ ~DockItem();
+
+ Gtk::Widget& getWidget();
+ GtkWidget *gobj();
+
+ Gtk::VBox *get_vbox();
+
+ void get_position(int& x, int& y);
+ void get_size(int& width, int& height);
+
+ void resize(int width, int height);
+ void move(int x, int y);
+ void set_position(Gtk::WindowPosition);
+ void set_size_request(int width, int height);
+ void size_request(Gtk::Requisition& requisition);
+ void set_title(Glib::ustring title);
+
+ bool isAttached() const;
+ bool isFloating() const;
+ bool isIconified() const;
+ State getState() const;
+ State getPrevState() const;
+ GdlDockPlacement getPlacement() const;
+
+ Gtk::Window *getWindow(); //< gives the parent window, if the dock item has one (i.e. it's floating)
+
+ void hide();
+ void show();
+ void iconify();
+ void show_all();
+
+ void present();
+
+ void grab_focus();
+
+ Glib::SignalProxy0<void> signal_show();
+ Glib::SignalProxy0<void> signal_hide();
+ Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event();
+ Glib::SignalProxy0<void> signal_drag_begin();
+ Glib::SignalProxy1<void, bool> signal_drag_end();
+ Glib::SignalProxy0<void> signal_realize();
+
+ sigc::signal<void, State, State> signal_state_changed();
+
+private:
+ Dock &_dock; //< parent dock
+
+ State _prev_state; //< last known state
+
+ int _prev_position;
+
+ Gtk::Window *_window; //< reference to floating window, if any
+ int _x, _y; //< last known position of window, if floating
+
+ bool _grab_focus_on_realize; //< if the dock item should grab focus on the next realize
+
+ GtkWidget *_gdl_dock_item;
+ Glib::RefPtr<Gdk::Pixbuf> _icon_pixbuf;
+
+ /** Interface widgets, will be packed like
+ * gdl_dock_item -> _frame -> _dock_item_box
+ */
+ Gtk::Frame _frame;
+ Gtk::VBox _dock_item_box;
+
+ /** Internal signal handlers */
+ void _onHide();
+ void _onHideWindow();
+ void _onShow();
+ void _onDragBegin();
+ void _onDragEnd(bool cancelled);
+ void _onRealize();
+
+ bool _onKeyPress(GdkEventKey *event);
+ void _onStateChanged(State prev_state, State new_state);
+ bool _onDeleteEvent(GdkEventAny *event);
+
+ sigc::connection _signal_key_press_event_connection;
+
+ /** GdlDockItem signal proxy structures */
+ static const Glib::SignalProxyInfo _signal_show_proxy;
+ static const Glib::SignalProxyInfo _signal_hide_proxy;
+ static const Glib::SignalProxyInfo _signal_delete_event_proxy;
+
+ static const Glib::SignalProxyInfo _signal_drag_begin_proxy;
+ static const Glib::SignalProxyInfo _signal_drag_end_proxy;
+ static const Glib::SignalProxyInfo _signal_realize_proxy;
+
+ static gboolean _signal_delete_event_callback(GtkWidget *self, GdkEventAny *event, void *data);
+ static void _signal_drag_end_callback(GtkWidget* self, gboolean p0, void* data);
+
+ sigc::signal<void, State, State> _signal_state_changed;
+
+ DockItem() = delete;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIGET_DOCK_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/widget/dock.cpp b/src/ui/widget/dock.cpp
new file mode 100644
index 0000000..9720296
--- /dev/null
+++ b/src/ui/widget/dock.cpp
@@ -0,0 +1,305 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A desktop dock pane to dock dialogs.
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2007 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 "dock.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "desktop.h"
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+void hideCallback(GObject * /*object*/, gpointer dock_ptr)
+{
+ g_return_if_fail( dock_ptr != nullptr );
+
+ Dock *dock = static_cast<Dock *>(dock_ptr);
+ dock->hide();
+}
+
+void unhideCallback(GObject * /*object*/, gpointer dock_ptr)
+{
+ g_return_if_fail( dock_ptr != nullptr );
+
+ Dock *dock = static_cast<Dock *>(dock_ptr);
+ dock->show();
+}
+
+}
+
+const int Dock::_default_empty_width = 0;
+const int Dock::_default_dock_bar_width = 36;
+
+
+Dock::Dock(Gtk::Orientation orientation)
+ : _gdl_dock(gdl_dock_new()),
+#if WITH_GDL_3_6
+ _gdl_dock_bar(GDL_DOCK_BAR(gdl_dock_bar_new(G_OBJECT(_gdl_dock)))),
+#else
+ _gdl_dock_bar(GDL_DOCK_BAR(gdl_dock_bar_new(GDL_DOCK(_gdl_dock)))),
+#endif
+ _scrolled_window (Gtk::manage(new Gtk::ScrolledWindow))
+{
+ gtk_widget_set_name(_gdl_dock, "GdlDock");
+
+#if WITH_GDL_3_6
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(_gdl_dock_bar),
+ static_cast<GtkOrientation>(orientation));
+#else
+ gdl_dock_bar_set_orientation(_gdl_dock_bar,
+ static_cast<GtkOrientation>(orientation));
+#endif
+
+ _filler.set_name("DockBoxFiller");
+
+ _paned = Gtk::manage(new Gtk::Paned(orientation));
+ _paned->set_name("DockBoxPane");
+ _paned->pack1(*Glib::wrap(GTK_WIDGET(_gdl_dock)), false, false);
+ _paned->pack2(_filler, true, false);
+ // resize, shrink
+
+ _dock_box = Gtk::manage(new Gtk::Box(orientation == Gtk::ORIENTATION_HORIZONTAL ?
+ Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL));
+ _dock_box->set_name("DockBox");
+ _dock_box->pack_start(*_paned, Gtk::PACK_EXPAND_WIDGET);
+ _dock_box->pack_end(*Gtk::manage(Glib::wrap(GTK_WIDGET(_gdl_dock_bar))), Gtk::PACK_SHRINK);
+
+ _scrolled_window->set_name("DockScrolledWindow");
+ _scrolled_window->add(*_dock_box);
+ _scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ _scrolled_window->set_size_request(0);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ GdlSwitcherStyle gdl_switcher_style =
+ static_cast<GdlSwitcherStyle>(prefs->getIntLimited("/options/dock/switcherstyle",
+ GDL_SWITCHER_STYLE_BOTH, 0, 4));
+
+ GdlDockMaster* master = nullptr;
+
+ g_object_get(GDL_DOCK_OBJECT(_gdl_dock),
+ "master", &master,
+ NULL);
+
+ g_object_set(master,
+ "switcher-style", gdl_switcher_style,
+ NULL);
+
+ GdlDockBarStyle gdl_dock_bar_style =
+ static_cast<GdlDockBarStyle>(prefs->getIntLimited("/options/dock/dockbarstyle",
+ GDL_DOCK_BAR_BOTH, 0, 3));
+
+ gdl_dock_bar_set_style(_gdl_dock_bar, gdl_dock_bar_style);
+
+
+ INKSCAPE.signal_dialogs_hide.connect(sigc::mem_fun(*this, &Dock::hide));
+ INKSCAPE.signal_dialogs_unhide.connect(sigc::mem_fun(*this, &Dock::show));
+
+ g_signal_connect(_paned->gobj(), "button-press-event", G_CALLBACK(_on_paned_button_event), (void *)this);
+ g_signal_connect(_paned->gobj(), "button-release-event", G_CALLBACK(_on_paned_button_event), (void *)this);
+
+ signal_layout_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::Dock::_onLayoutChanged));
+}
+
+Dock::~Dock()
+{
+ g_free(_gdl_dock);
+ g_free(_gdl_dock_bar);
+}
+
+void Dock::addItem(DockItem& item, GdlDockPlacement placement)
+{
+ _dock_items.push_back(&item);
+
+ gdl_dock_add_item(GDL_DOCK(_gdl_dock),
+ GDL_DOCK_ITEM(item.gobj()),
+ placement);
+}
+
+Gtk::Widget &Dock::getWidget()
+{
+ return *_scrolled_window;
+}
+
+Gtk::Paned *Dock::getParentPaned()
+{
+ g_return_val_if_fail(_dock_box, 0);
+ Gtk::Container *parent = getWidget().get_parent();
+ return (parent != nullptr ? dynamic_cast<Gtk::Paned *>(parent) : nullptr);
+}
+
+
+Gtk::Paned *Dock::getPaned()
+{
+ return _paned;
+}
+
+GtkWidget *Dock::getGdlWidget()
+{
+ return GTK_WIDGET(_gdl_dock);
+}
+
+bool Dock::isEmpty() const
+{
+ std::list<const DockItem *>::const_iterator
+ i = _dock_items.begin(),
+ e = _dock_items.end();
+
+ for (; i != e; ++i) {
+ if ((*i)->getState() == DockItem::DOCKED_STATE) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool Dock::hasIconifiedItems() const
+{
+ std::list<const DockItem *>::const_iterator
+ i = _dock_items.begin(),
+ e = _dock_items.end();
+
+ for (; i != e; ++i) {
+ if ((*i)->isIconified()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Dock::hide()
+{
+ getWidget().hide();
+}
+
+void Dock::show()
+{
+ getWidget().show();
+}
+
+void Dock::toggleDockable(int width, int height)
+{
+ static int prev_horizontal_position, prev_vertical_position;
+
+ Gtk::Paned *parent_paned = getParentPaned();
+
+ if (width > 0 && height > 0) {
+ prev_horizontal_position = parent_paned->get_position();
+ prev_vertical_position = _paned->get_position();
+
+ if (getWidget().get_width() < width)
+ parent_paned->set_position(parent_paned->get_width() - width);
+
+ if (_paned->get_position() < height)
+ _paned->set_position(height);
+
+ } else {
+ parent_paned->set_position(prev_horizontal_position);
+ _paned->set_position(prev_vertical_position);
+ }
+}
+
+void Dock::scrollToItem(DockItem& item)
+{
+ int item_x, item_y;
+ item.getWidget().translate_coordinates(getWidget(), 0, 0, item_x, item_y);
+
+ int dock_height = getWidget().get_height(), item_height = item.getWidget().get_height();
+ double vadjustment = _scrolled_window->get_vadjustment()->get_value();
+
+ if (item_y < 0)
+ _scrolled_window->get_vadjustment()->set_value(vadjustment + item_y);
+ else if (item_y + item_height > dock_height)
+ _scrolled_window->get_vadjustment()->set_value(
+ vadjustment + ((item_y + item_height) - dock_height));
+}
+
+Glib::SignalProxy0<void>
+Dock::signal_layout_changed()
+{
+ return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock)),
+ &_signal_layout_changed_proxy);
+}
+
+void Dock::_onLayoutChanged()
+{
+ if (isEmpty()) {
+ if (hasIconifiedItems()) {
+ _paned->get_child1()->set_size_request(-1, -1);
+ _scrolled_window->set_size_request(_default_dock_bar_width);
+ } else {
+ _paned->get_child1()->set_size_request(-1, -1);
+ _scrolled_window->set_size_request(_default_empty_width);
+ }
+ getParentPaned()->set_position(10000);
+
+ } else {
+ // unset any forced size requests
+ _paned->get_child1()->set_size_request(-1, -1);
+ _scrolled_window->set_size_request(-1);
+ }
+}
+
+void
+Dock::_onPanedButtonEvent(GdkEventButton *event)
+{
+ if (event->button == 1 && event->type == GDK_BUTTON_PRESS)
+ /* unset size request when starting a drag */
+ _paned->get_child1()->set_size_request(-1, -1);
+}
+
+gboolean
+Dock::_on_paned_button_event(GtkWidget */*widget*/, GdkEventButton *event, gpointer user_data)
+{
+ if (Dock *dock = static_cast<Dock *>(user_data))
+ dock->_onPanedButtonEvent(event);
+
+ return FALSE;
+}
+
+const Glib::SignalProxyInfo
+Dock::_signal_layout_changed_proxy =
+{
+ "layout-changed",
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback,
+ (GCallback) &Glib::SignalProxyNormal::slot0_void_callback
+};
+
+
+} // namespace Widget
+} // 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/widget/dock.h b/src/ui/widget/dock.h
new file mode 100644
index 0000000..f061f59
--- /dev/null
+++ b/src/ui/widget/dock.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A desktop dock pane to dock dialogs, a custom wrapper around gdl-dock.
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_DOCK_H
+#define INKSCAPE_UI_WIDGET_DOCK_H
+
+#include <gtkmm/box.h>
+#include <list>
+#include "ui/widget/dock-item.h"
+
+struct _GdlDock;
+typedef _GdlDock GdlDock;
+struct _GdlDockBar;
+typedef _GdlDockBar GdlDockBar;
+
+namespace Gtk {
+class Paned;
+class ScrolledWindow;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Dock {
+
+public:
+
+ Dock(Gtk::Orientation orientation=Gtk::ORIENTATION_VERTICAL);
+ ~Dock();
+
+ void addItem(DockItem& item, GdlDockPlacement placement);
+
+ Gtk::Widget& getWidget(); //< return the top widget
+ Gtk::Paned *getParentPaned();
+ Gtk::Paned *getPaned();
+
+ GtkWidget* getGdlWidget(); //< return the top gdl widget
+
+ bool isEmpty() const; //< true iff none of the dock's items are in a docked state
+ bool hasIconifiedItems() const;
+
+ Glib::SignalProxy0<void> signal_layout_changed();
+
+ void hide();
+ void show();
+
+ /** Toggle size of dock between the previous dimensions and the ones sent as parameters */
+ void toggleDockable(int width=0, int height=0);
+
+ /** Scrolls the scrolled window container to make the provided dock item visible, if needed */
+ void scrollToItem(DockItem& item);
+
+protected:
+
+ std::list<const DockItem *> _dock_items; //< added dock items
+
+ /** Interface widgets, will be packed like
+ * _scrolled_window -> (_dock_box -> (_paned -> (_dock -> _filler) | _dock_bar))
+ */
+ Gtk::Box *_dock_box;
+ Gtk::Paned *_paned;
+ GtkWidget *_gdl_dock;
+ GdlDockBar *_gdl_dock_bar;
+ Gtk::Box _filler;
+ Gtk::ScrolledWindow *_scrolled_window;
+
+ /** Internal signal handlers */
+ void _onLayoutChanged();
+ void _onPanedButtonEvent(GdkEventButton *event);
+
+ static gboolean _on_paned_button_event(GtkWidget *widget, GdkEventButton *event,
+ gpointer user_data);
+
+ /** GdlDock signal proxy structures */
+ static const Glib::SignalProxyInfo _signal_layout_changed_proxy;
+
+ /** Standard widths */
+ static const int _default_empty_width;
+ static const int _default_dock_bar_width;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_BEHAVIOUR_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/widget/entity-entry.cpp b/src/ui/widget/entity-entry.cpp
new file mode 100644
index 0000000..6e87459
--- /dev/null
+++ b/src/ui/widget/entity-entry.cpp
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "entity-entry.h"
+
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/entry.h>
+
+#include "document-undo.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "rdf.h"
+#include "verbs.h"
+
+#include "object/sp-root.h"
+
+#include "ui/widget/registry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+EntityEntry*
+EntityEntry::create (rdf_work_entity_t* ent, Registry& wr)
+{
+ g_assert (ent);
+ EntityEntry* obj = nullptr;
+ switch (ent->format)
+ {
+ case RDF_FORMAT_LINE:
+ obj = new EntityLineEntry (ent, wr);
+ break;
+ case RDF_FORMAT_MULTILINE:
+ obj = new EntityMultiLineEntry (ent, wr);
+ break;
+ default:
+ g_warning ("An unknown RDF format was requested.");
+ }
+
+ g_assert (obj);
+ obj->_label.show();
+ return obj;
+}
+
+EntityEntry::EntityEntry (rdf_work_entity_t* ent, Registry& wr)
+ : _label(Glib::ustring(_(ent->title)), Gtk::ALIGN_END),
+ _packable(nullptr),
+ _entity(ent), _wr(&wr)
+{
+}
+
+EntityEntry::~EntityEntry()
+{
+ _changed_connection.disconnect();
+}
+
+void EntityEntry::save_to_preferences(SPDocument *doc)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ const gchar *text = rdf_get_work_entity (doc, _entity);
+ prefs->setString(PREFS_METADATA + Glib::ustring(_entity->name), Glib::ustring(text ? text : ""));
+}
+
+EntityLineEntry::EntityLineEntry (rdf_work_entity_t* ent, Registry& wr)
+: EntityEntry (ent, wr)
+{
+ Gtk::Entry *e = new Gtk::Entry;
+ e->set_tooltip_text (_(ent->tip));
+ _packable = e;
+ _changed_connection = e->signal_changed().connect (sigc::mem_fun (*this, &EntityLineEntry::on_changed));
+}
+
+EntityLineEntry::~EntityLineEntry()
+{
+ delete static_cast<Gtk::Entry*>(_packable);
+}
+
+void EntityLineEntry::update(SPDocument *doc)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ static_cast<Gtk::Entry*>(_packable)->set_text (text ? text : "");
+}
+
+
+void EntityLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ static_cast<Gtk::Entry*>(_packable)->set_text (text.c_str());
+ }
+}
+
+void
+EntityLineEntry::on_changed()
+{
+ if (_wr->isUpdating()) return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ Glib::ustring text = static_cast<Gtk::Entry*>(_packable)->get_text();
+ if (rdf_set_work_entity (doc, _entity, text.c_str())) {
+ if (doc->isSensitive()) {
+ DocumentUndo::done(doc, SP_VERB_NONE, "Document metadata updated");
+ }
+ }
+ _wr->setUpdating (false);
+}
+
+EntityMultiLineEntry::EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr)
+: EntityEntry (ent, wr)
+{
+ Gtk::ScrolledWindow *s = new Gtk::ScrolledWindow;
+ s->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ s->set_shadow_type (Gtk::SHADOW_IN);
+ _packable = s;
+ _v.set_size_request (-1, 35);
+ _v.set_wrap_mode (Gtk::WRAP_WORD);
+ _v.set_accepts_tab (false);
+ s->add (_v);
+ _v.set_tooltip_text (_(ent->tip));
+ _changed_connection = _v.get_buffer()->signal_changed().connect (sigc::mem_fun (*this, &EntityMultiLineEntry::on_changed));
+}
+
+EntityMultiLineEntry::~EntityMultiLineEntry()
+{
+ delete static_cast<Gtk::ScrolledWindow*>(_packable);
+}
+
+void EntityMultiLineEntry::update(SPDocument *doc)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text ? text : "");
+}
+
+
+void EntityMultiLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text.c_str());
+ }
+}
+
+
+void
+EntityMultiLineEntry::on_changed()
+{
+ if (_wr->isUpdating()) return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ Glib::ustring text = tv->get_buffer()->get_text();
+ if (rdf_set_work_entity (doc, _entity, text.c_str())) {
+ DocumentUndo::done(doc, SP_VERB_NONE, "Document metadata updated");
+ }
+ _wr->setUpdating (false);
+}
+
+} // 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/widget/entity-entry.h b/src/ui/widget/entity-entry.h
new file mode 100644
index 0000000..3168e4c
--- /dev/null
+++ b/src/ui/widget/entity-entry.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+
+#include <gtkmm/textview.h>
+
+struct rdf_work_entity_t;
+class SPDocument;
+
+namespace Gtk {
+class TextBuffer;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+class EntityEntry {
+public:
+ static EntityEntry* create (rdf_work_entity_t* ent, Registry& wr);
+ virtual ~EntityEntry() = 0;
+ virtual void update (SPDocument *doc) = 0;
+ virtual void on_changed() = 0;
+ virtual void load_from_preferences() = 0;
+ void save_to_preferences(SPDocument *doc);
+ Gtk::Label _label;
+ Gtk::Widget *_packable;
+
+protected:
+ EntityEntry (rdf_work_entity_t* ent, Registry& wr);
+ sigc::connection _changed_connection;
+ rdf_work_entity_t *_entity;
+ Registry *_wr;
+};
+
+class EntityLineEntry : public EntityEntry {
+public:
+ EntityLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+};
+
+class EntityMultiLineEntry : public EntityEntry {
+public:
+ EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityMultiLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+ Gtk::TextView _v;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTITY_ENTRY__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/widget/entry.cpp b/src/ui/widget/entry.cpp
new file mode 100644
index 0000000..e9a63c5
--- /dev/null
+++ b/src/ui/widget/entry.cpp
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "entry.h"
+
+#include <gtkmm/entry.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Entry::Entry( Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic)
+{
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
diff --git a/src/ui/widget/entry.h b/src/ui/widget/entry.h
new file mode 100644
index 0000000..3674d51
--- /dev/null
+++ b/src/ui/widget/entry.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTRY__H
+
+#include "labelled.h"
+
+namespace Gtk {
+class Entry;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Helperclass for Gtk::Entry widgets.
+ */
+class Entry : public Labelled
+{
+public:
+ Entry( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ // TO DO: add methods to access Gtk::Entry widget
+
+ Gtk::Entry* getEntry() {return (Gtk::Entry*)(_widget);};
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTRY__H
diff --git a/src/ui/widget/filter-effect-chooser.cpp b/src/ui/widget/filter-effect-chooser.cpp
new file mode 100644
index 0000000..d933408
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.cpp
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Filter effect selection selection widget
+ *
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2007, 2017 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "filter-effect-chooser.h"
+
+#include "document.h"
+
+namespace Inkscape {
+
+const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_ENDMODE] = {
+ { SP_CSS_BLEND_NORMAL, _("Normal"), "normal" },
+ { SP_CSS_BLEND_MULTIPLY, _("Multiply"), "multiply" },
+ { SP_CSS_BLEND_SCREEN, _("Screen"), "screen" },
+ { SP_CSS_BLEND_DARKEN, _("Darken"), "darken" },
+ { SP_CSS_BLEND_LIGHTEN, _("Lighten"), "lighten" },
+ // New in Compositing and Blending Level 1
+ { SP_CSS_BLEND_OVERLAY, _("Overlay"), "overlay" },
+ { SP_CSS_BLEND_COLORDODGE, _("Color Dodge"), "color-dodge" },
+ { SP_CSS_BLEND_COLORBURN, _("Color Burn"), "color-burn" },
+ { SP_CSS_BLEND_HARDLIGHT, _("Hard Light"), "hard-light" },
+ { SP_CSS_BLEND_SOFTLIGHT, _("Soft Light"), "soft-light" },
+ { SP_CSS_BLEND_DIFFERENCE, _("Difference"), "difference" },
+ { SP_CSS_BLEND_EXCLUSION, _("Exclusion"), "exclusion" },
+ { SP_CSS_BLEND_HUE, _("Hue"), "hue" },
+ { SP_CSS_BLEND_SATURATION, _("Saturation"), "saturation" },
+ { SP_CSS_BLEND_COLOR, _("Color"), "color" },
+ { SP_CSS_BLEND_LUMINOSITY, _("Luminosity"), "luminosity" }
+};
+const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_ENDMODE);
+
+
+namespace UI {
+namespace Widget {
+
+SimpleFilterModifier::SimpleFilterModifier(int flags)
+ : _flags(flags)
+ , _lb_blend(_("Blend mode:"))
+ , _lb_isolation("Isolate") // Translate for 1.1
+ , _blend(SPBlendModeConverter, SP_ATTR_INVALID, false)
+ , _blur(_("Blur (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _opacity(_("Opacity (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _notify(true)
+{
+ set_name("SimpleFilterModifier");
+
+ _flags = flags;
+
+ if (flags & BLEND) {
+ add(_hb_blend);
+ _lb_blend.set_use_underline();
+ _hb_blend.set_halign(Gtk::ALIGN_END);
+ _hb_blend.set_valign(Gtk::ALIGN_CENTER);
+ _hb_blend.set_margin_top(3);
+ _hb_blend.set_margin_end(5);
+ _lb_blend.set_mnemonic_widget(_blend);
+ _hb_blend.pack_start(_lb_blend, false, false, 5);
+ _hb_blend.pack_start(_blend, false, false, 5);
+ /*
+ * For best fit inkscape-browsers with no GUI to isolation we need all groups,
+ * clones and symbols with isolation == isolate to not show to the user of
+ * Inkscape a "strange" behabiour from the designer point of view.
+ * Is strange because only happends when object not has clip, mask,
+ * filter, blending or opacity .
+ * Anyway the feature is a no-gui feature and render as spected.
+ */
+ /* if (flags & ISOLATION) {
+ _isolation.property_active() = false;
+ _hb_blend.pack_start(_isolation, false, false, 5);
+ _hb_blend.pack_start(_lb_isolation, false, false, 5);
+ _isolation.set_tooltip_text("Don't blend childrens with objects behind");
+ _lb_isolation.set_tooltip_text("Don't blend childrens with objects behind");
+ } */
+ Gtk::Separator *separator = Gtk::manage(new Gtk::Separator());
+ separator->set_margin_top(8);
+ separator->set_margin_bottom(8);
+ add(*separator);
+ }
+
+ if (flags & BLUR) {
+ add(_blur);
+ }
+
+ if (flags & OPACITY) {
+ add(_opacity);
+ }
+ show_all_children();
+
+ _blend.signal_changed().connect(signal_blend_changed());
+ _blur.signal_value_changed().connect(signal_blur_changed());
+ _opacity.signal_value_changed().connect(signal_opacity_changed());
+ _isolation.signal_toggled().connect(signal_isolation_changed());
+}
+
+sigc::signal<void> &SimpleFilterModifier::signal_isolation_changed()
+{
+ if (_notify) {
+ return _signal_isolation_changed;
+ }
+ _notify = true;
+ return _signal_null;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_blend_changed()
+{
+ if (_notify) {
+ return _signal_blend_changed;
+ }
+ _notify = true;
+ return _signal_null;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_blur_changed()
+{
+ // we dont use notifi to block use aberaje for multiple
+ return _signal_blur_changed;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_opacity_changed()
+{
+ // we dont use notifi to block use averaje for multiple
+ return _signal_opacity_changed;
+}
+
+SPIsolation SimpleFilterModifier::get_isolation_mode()
+{
+ return _isolation.get_active() ? SP_CSS_ISOLATION_ISOLATE : SP_CSS_ISOLATION_AUTO;
+}
+
+void SimpleFilterModifier::set_isolation_mode(const SPIsolation val, bool notify)
+{
+ _notify = notify;
+ _isolation.set_active(val == SP_CSS_ISOLATION_ISOLATE);
+}
+
+SPBlendMode SimpleFilterModifier::get_blend_mode()
+{
+ const Util::EnumData<SPBlendMode> *d = _blend.get_active_data();
+ if (d) {
+ return _blend.get_active_data()->id;
+ } else {
+ return SP_CSS_BLEND_NORMAL;
+ }
+}
+
+void SimpleFilterModifier::set_blend_mode(const SPBlendMode val, bool notify)
+{
+ _notify = notify;
+ _blend.set_active(val);
+}
+
+double SimpleFilterModifier::get_blur_value() const
+{
+ return _blur.get_value();
+}
+
+void SimpleFilterModifier::set_blur_value(const double val)
+{
+ _blur.set_value(val);
+}
+
+double SimpleFilterModifier::get_opacity_value() const
+{
+ return _opacity.get_value();
+}
+
+void SimpleFilterModifier::set_opacity_value(const double val)
+{
+ _opacity.set_value(val);
+}
+
+}
+}
+}
+
+/*
+ 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/widget/filter-effect-chooser.h b/src/ui/widget/filter-effect-chooser.h
new file mode 100644
index 0000000..cbbe2b5
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.h
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __FILTER_EFFECT_CHOOSER_H__
+#define __FILTER_EFFECT_CHOOSER_H__
+
+/*
+ * Filter effect selection selection widget
+ *
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2007, 2017 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/separator.h>
+
+#include "combo-enums.h"
+#include "spin-scale.h"
+#include "style-enums.h"
+
+using Inkscape::Util::EnumData;
+using Inkscape::Util::EnumDataConverter;
+
+namespace Inkscape {
+extern const Util::EnumDataConverter<SPBlendMode> SPBlendModeConverter;
+namespace UI {
+namespace Widget {
+
+/* Allows basic control over feBlend and feGaussianBlur effects as well as opacity.
+ * Common for Object, Layers, and Fill and Stroke dialogs.
+*/
+class SimpleFilterModifier : public Gtk::VBox
+{
+public:
+ enum Flags { NONE = 0, BLUR = 1, OPACITY = 2, BLEND = 4, ISOLATION = 16 };
+
+ SimpleFilterModifier(int flags);
+
+ sigc::signal<void> &signal_blend_changed();
+ sigc::signal<void> &signal_blur_changed();
+ sigc::signal<void> &signal_opacity_changed();
+ sigc::signal<void> &signal_isolation_changed();
+
+ SPIsolation get_isolation_mode();
+ void set_isolation_mode(const SPIsolation, bool notify);
+
+ SPBlendMode get_blend_mode();
+ void set_blend_mode(const SPBlendMode, bool notify);
+
+ double get_blur_value() const;
+ void set_blur_value(const double);
+
+ double get_opacity_value() const;
+ void set_opacity_value(const double);
+
+private:
+ int _flags;
+ bool _notify;
+
+ Gtk::HBox _hb_blend;
+ Gtk::Label _lb_blend;
+ Gtk::Label _lb_isolation;
+ ComboBoxEnum<SPBlendMode> _blend;
+ SpinScale _blur;
+ SpinScale _opacity;
+ Gtk::CheckButton _isolation;
+
+ sigc::signal<void> _signal_null;
+ sigc::signal<void> _signal_blend_changed;
+ sigc::signal<void> _signal_blur_changed;
+ sigc::signal<void> _signal_opacity_changed;
+ sigc::signal<void> _signal_isolation_changed;
+};
+
+}
+}
+}
+
+#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/widget/font-button.cpp b/src/ui/widget/font-button.cpp
new file mode 100644
index 0000000..e0a140a
--- /dev/null
+++ b/src/ui/widget/font-button.cpp
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "font-button.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/fontbutton.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontButton::FontButton(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::FontButton("Sans 10"), suffix, icon, mnemonic)
+{
+}
+
+Glib::ustring FontButton::getValue() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::FontButton*>(_widget)->get_font_name();
+}
+
+
+void FontButton::setValue (Glib::ustring fontspec)
+{
+ g_assert(_widget != nullptr);
+ static_cast<Gtk::FontButton*>(_widget)->set_font_name(fontspec);
+}
+
+Glib::SignalProxy0<void> FontButton::signal_font_value_changed()
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::FontButton*>(_widget)->signal_font_set();
+}
+
+
+} // namespace Widget
+} // 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/widget/font-button.h b/src/ui/widget/font-button.h
new file mode 100644
index 0000000..a53b7d6
--- /dev/null
+++ b/src/ui/widget/font-button.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_BUTTON_H
+#define INKSCAPE_UI_WIDGET_FONT_BUTTON_H
+
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled font button for entering font values
+ */
+class FontButton : public Labelled
+{
+public:
+ /**
+ * Construct a FontButton Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ FontButton( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ Glib::ustring getValue() const;
+ void setValue (Glib::ustring fontspec);
+ /**
+ * Signal raised when the font button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_font_value_changed();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RANDOM_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/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp
new file mode 100644
index 0000000..ea53d90
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.cpp
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+#include <gdkmm/display.h>
+
+#include "font-selector-toolbar.h"
+
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-instance.h"
+
+#include "ui/icon-names.h"
+
+// For updating from selection
+#include "inkscape.h"
+#include "desktop.h"
+#include "object/sp-text.h"
+
+// TEMP TEMP TEMP
+#include "ui/toolbar/text-toolbar.h"
+
+/* To do:
+ * Fix altx. Need to store
+ */
+
+void family_cell_data_func(const Gtk::TreeModel::const_iterator iter, Gtk::CellRendererText* cell ) {
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::ustring markup = font_lister->get_font_family_markup(iter);
+ // std::cout << "Markup: " << markup << std::endl;
+
+ cell->set_property ("markup", markup);
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontSelectorToolbar::FontSelectorToolbar ()
+ : Gtk::Grid ()
+ , family_combo (true) // true => with text entry.
+ , style_combo (true)
+ , signal_block (false)
+{
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // Font family
+ family_combo.set_model (font_lister->get_font_list());
+ family_combo.set_entry_text_column (0);
+ family_combo.set_name ("FontSelectorToolBar: Family");
+ family_combo.set_row_separator_func (&font_lister_separator_func);
+
+ family_combo.clear(); // Clears all CellRenderer mappings.
+ family_combo.set_cell_data_func (family_cell,
+ sigc::bind(sigc::ptr_fun(family_cell_data_func), &family_cell));
+ family_combo.pack_start (family_cell);
+
+
+ Gtk::Entry* entry = family_combo.get_entry();
+ entry->signal_icon_press().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_icon_pressed));
+ entry->signal_key_press_event().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_key_press_event), false); // false => connect first
+ entry->set_data (Glib::Quark("altx-text"), entry); // Desktop will set focus to entry with Alt-x.
+
+
+ Glib::RefPtr<Gtk::EntryCompletion> completion = Gtk::EntryCompletion::create();
+ completion->set_model (font_lister->get_font_list());
+ completion->set_text_column (0);
+ completion->set_popup_completion ();
+ completion->set_inline_completion (false);
+ completion->set_inline_selection ();
+ // completion->signal_match_selected().connect(sigc::mem_fun(*this, &FontSelectorToolbar::on_match_selected), false); // false => connect before default handler.
+ entry->set_completion (completion);
+
+ // Style
+ style_combo.set_model (font_lister->get_style_list());
+ style_combo.set_name ("FontSelectorToolbar: Style");
+
+ // Grid
+ set_name ("FontSelectorToolbar: Grid");
+ attach (family_combo, 0, 0, 1, 1);
+ attach (style_combo, 1, 0, 1, 1);
+
+ // Add signals
+ family_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_family_changed));
+ style_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_style_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+
+ // When FontLister is changed, update family and style shown in GUI.
+ font_lister->connectUpdate(sigc::mem_fun(*this, &FontSelectorToolbar::update_font));
+}
+
+
+// Update GUI based on font-selector values.
+void
+FontSelectorToolbar::update_font ()
+{
+ if (signal_block) return;
+
+ signal_block = true;
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreeModel::Row row;
+
+ // Set font family.
+ try {
+ row = font_lister->get_row_for_font ();
+ family_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for family: "
+ << font_lister->get_font_family() << std::endl;
+ }
+
+ // Set style.
+ try {
+ row = font_lister->get_row_for_style ();
+ style_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for style: "
+ << font_lister->get_font_style() << std::endl;
+ }
+
+ // Check for missing fonts.
+ Glib::ustring missing_fonts = get_missing_fonts();
+
+ // Add an icon to end of entry.
+ Gtk::Entry* entry = family_combo.get_entry();
+ if (missing_fonts.empty()) {
+ // If no missing fonts, add icon for selecting all objects with this font-family.
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("edit-select-all"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (_("Select all text with this text family"), Gtk::ENTRY_ICON_SECONDARY);
+ } else {
+ // If missing fonts, add warning icon.
+ Glib::ustring warning = _("Font not found on system: ") + missing_fonts;
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("dialog-warning"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (warning, Gtk::ENTRY_ICON_SECONDARY);
+ }
+
+ signal_block = false;
+}
+
+// Get comma separated list of fonts in font-family that are not on system.
+// To do, move to font-lister.
+Glib::ustring
+FontSelectorToolbar::get_missing_fonts ()
+{
+ // Get font list in text entry which may be a font stack (with fallbacks).
+ Glib::ustring font_list = family_combo.get_entry_text();
+ Glib::ustring missing_font_list;
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", font_list);
+
+ for (auto token: tokens) {
+ bool found = false;
+ Gtk::TreeModel::Children children = font_lister->get_font_list()->children();
+ for (auto iter2: children) {
+ Gtk::TreeModel::Row row2 = *iter2;
+ Glib::ustring family2 = row2[font_lister->FontList.family];
+ bool onSystem2 = row2[font_lister->FontList.onSystem];
+ // CSS dictates that font family names are case insensitive.
+ // This should really implement full Unicode case unfolding.
+ if (onSystem2 && token.casefold().compare(family2.casefold()) == 0) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ missing_font_list += token;
+ missing_font_list += ", ";
+ }
+ }
+
+ // Remove extra comma and space from end.
+ if (missing_font_list.size() >= 2) {
+ missing_font_list.resize(missing_font_list.size() - 2);
+ }
+
+ return missing_font_list;
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelectorToolbar::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring family = family_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_family (family);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_style_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring style = style_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_style (style);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event) {
+ std::cout << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl;
+ std::cout << " .... Should select all items with same font-family. FIXME" << std::endl;
+ // Call equivalent of sp_text_toolbox_select_cb() in text-toolbar.cpp
+ // Should be action! (Maybe: select_all_fontfamily( Glib::ustring font_family );).
+ // Check how Find dialog works.
+}
+
+// bool
+// FontSelectorToolbar::on_match_selected (const Gtk::TreeModel::iterator& iter)
+// {
+// std::cout << "on_match_selected" << std::endl;
+// std::cout << " FIXME" << std::endl;
+// Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+// Glib::ustring family = (*iter)[font_lister->FontList.family];
+// std::cout << " family: " << family << std::endl;
+// return false; // Leave it to default handler to set entry text.
+// }
+
+// Return focus to canvas.
+bool
+FontSelectorToolbar::on_key_press_event (GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ switch ( key ) {
+
+ case GDK_KEY_Escape:
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ // Defocus
+ std::cerr << "FontSelectorToolbar::on_key_press_event: Defocus: FIXME" << std::endl;
+ consumed = true;
+ }
+ break;
+ }
+
+ return consumed; // Leave it to default handler if false.
+}
+
+void
+FontSelectorToolbar::changed_emit() {
+ signal_block = true;
+ changed_signal.emit ();
+ signal_block = false;
+}
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/font-selector-toolbar.h b/src/ui/widget/font-selector-toolbar.h
new file mode 100644
index 0000000..53cdcea
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.h
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * The routines here create and manage a font selector widget with two parts,
+ * one each for font-family and font-style.
+ *
+ * This is essentially a toolbar version of the 'FontSelector' widget. Someday
+ * this may be merged with it.
+ *
+ * The main functions are:
+ * Create the font-selector toolbar widget.
+ * Update the lists when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Update the on-screen text.
+ * Provide the currently selected values.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/comboboxtext.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ *
+ * It is used by Text tool toolbar. The FontSelectorToolbar class utilizes the
+ * FontLister class to obtain a list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts
+ * are kept track of by their "fontspecs" which are the same as the strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-selector widget.
+ * Update the child widgets when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made to a child widget.
+ */
+class FontSelectorToolbar : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontSelectorToolbar ();
+
+protected:
+
+ // Font family
+ Gtk::ComboBox family_combo;
+ Gtk::CellRendererText family_cell;
+
+ // Font style
+ Gtk::ComboBoxText style_combo;
+ Gtk::CellRendererText style_cell;
+
+private:
+
+ // Make a list of missing fonts for tooltip and for warning icon.
+ Glib::ustring get_missing_fonts ();
+
+ // Signal handlers
+ void on_family_changed();
+ void on_style_changed();
+ void on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event);
+ // bool on_match_selected (const Gtk::TreeModel::iterator& iter);
+ bool on_key_press_event (GdkEventKey* key_event) override;
+
+ // Signals
+ sigc::signal<void> changed_signal;
+ void changed_emit();
+ bool signal_block;
+
+public:
+
+ /**
+ * Update GUI based on font-selector values.
+ */
+ void update_font ();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return changed_signal.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_TOOLBAR_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 :
diff --git a/src/ui/widget/font-selector.cpp b/src/ui/widget/font-selector.cpp
new file mode 100644
index 0000000..df13fa3
--- /dev/null
+++ b/src/ui/widget/font-selector.cpp
@@ -0,0 +1,450 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-selector.h"
+
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-instance.h"
+
+// For updating from selection
+#include "inkscape.h"
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontSelector::FontSelector (bool with_size, bool with_variations)
+ : Gtk::Grid ()
+ , family_frame (_("Font family"))
+ , style_frame (C_("Font selector", "Style"))
+ , size_label (_("Font size"))
+ , size_combobox (true) // With entry
+ , signal_block (false)
+ , font_size (18)
+{
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // Font family
+ family_treecolumn.pack_start (family_cell, false);
+ family_treecolumn.set_fixed_width (200);
+ family_treecolumn.add_attribute (family_cell, "text", 0);
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+
+ family_treeview.set_row_separator_func (&font_lister_separator_func);
+ family_treeview.set_model (font_lister->get_font_list());
+ family_treeview.set_name ("FontSelector: Family");
+ family_treeview.set_headers_visible (false);
+ family_treeview.append_column (family_treecolumn);
+
+ family_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ family_scroll.add (family_treeview);
+
+ family_frame.set_hexpand (true);
+ family_frame.set_vexpand (true);
+ family_frame.add (family_scroll);
+
+ // Style
+ style_treecolumn.pack_start (style_cell, false);
+ style_treecolumn.add_attribute (style_cell, "text", 0);
+ style_treecolumn.set_cell_data_func (style_cell, sigc::mem_fun(*this, &FontSelector::style_cell_data_func));
+ style_treecolumn.set_title ("Face");
+ style_treecolumn.set_resizable (true);
+
+ style_treeview.set_model (font_lister->get_style_list());
+ style_treeview.set_name ("FontSelectorStyle");
+ style_treeview.append_column ("CSS", font_lister->FontStyleList.cssStyle);
+ style_treeview.append_column (style_treecolumn);
+
+ style_treeview.get_column(0)->set_resizable (true);
+
+ style_scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ style_scroll.add (style_treeview);
+
+ style_frame.set_hexpand (true);
+ style_frame.set_vexpand (true);
+ style_frame.add (style_scroll);
+
+ // Size
+ size_combobox.set_name ("FontSelectorSize");
+ set_sizes();
+ size_combobox.set_active_text( "18" );
+
+ // Font Variations
+ font_variations.set_vexpand (true);
+ font_variations_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ font_variations_scroll.add (font_variations);
+
+ // Grid
+ set_name ("FontSelectorGrid");
+ set_row_spacing(4);
+ set_column_spacing(4);
+ // Add extra columns to the "family frame" to change space distribution
+ // by prioritizing font family over styles
+ const int extra = 4;
+ attach (family_frame, 0, 0, 1 + extra, 2);
+ attach (style_frame, 1 + extra, 0, 2, 1);
+ if (with_size) { // Glyph panel does not use size.
+ attach (size_label, 1 + extra, 1, 1, 1);
+ attach (size_combobox, 2 + extra, 1, 1, 1);
+ }
+ if (with_variations) { // Glyphs panel does not use variations.
+ attach (font_variations_scroll, 0, 2, 3 + extra, 1);
+ }
+
+ // Add signals
+ family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed));
+ style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed));
+ size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed));
+ font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+}
+
+void
+FontSelector::set_sizes ()
+{
+ size_combobox.remove_all();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+
+ int sizes[] = {
+ 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
+ 32, 36, 40, 48, 56, 64, 72, 144
+ };
+
+ // Array must be same length as SPCSSUnit in style-internal.h
+ // PX PT PC MM CM IN EM EX %
+ double ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16};
+
+ for (int i : sizes)
+ {
+ double size = i/ratios[unit];
+ size_combobox.append( Glib::ustring::format(size) );
+ }
+}
+
+void
+FontSelector::set_fontsize_tooltip()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")");
+ size_combobox.set_tooltip_text (tooltip);
+}
+
+// Update GUI.
+// We keep a private copy of the style list as the font-family in widget is only temporary
+// until the "Apply" button is set so the style list can be different from that in
+// FontLister.
+void
+FontSelector::update_font ()
+{
+ signal_block = true;
+
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreePath path;
+ Glib::ustring family = font_lister->get_font_family();
+ Glib::ustring style = font_lister->get_font_style();
+
+ // Set font family
+ try {
+ path = font_lister->get_row_for_font (family);
+ } catch (...) {
+ std::cerr << "FontSelector::update_font: Couldn't find row for font-family: "
+ << family << std::endl;
+ path.clear();
+ path.push_back(0);
+ }
+
+ Gtk::TreePath currentPath;
+ Gtk::TreeViewColumn *currentColumn;
+ family_treeview.get_cursor(currentPath, currentColumn);
+ if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) {
+ family_treeview.set_cursor (path);
+ family_treeview.scroll_to_row (path);
+ }
+
+ // Get font-lister style list for selected family
+ Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path));
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Copy font-lister style list to private list store, searching for match.
+ Gtk::TreeModel::iterator match;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (style == ((StyleNames*)styles->data)->CssName) {
+ match = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (match) {
+ style_treeview.get_selection()->select (match);
+ }
+
+ Glib::ustring fontspec = font_lister->get_fontspec();
+ update_variations(fontspec);
+
+ signal_block = false;
+}
+
+void
+FontSelector::update_size (double size)
+{
+ signal_block = true;
+
+ // Set font size
+ std::stringstream ss;
+ ss << size;
+ size_combobox.get_entry()->set_text( ss.str() );
+ font_size = size; // Store value
+ set_fontsize_tooltip();
+
+ signal_block = false;
+}
+
+
+// If use_variations is true (default), we get variation values from variations widget otherwise we
+// get values from CSS widget (we need to be able to keep the two widgets synchronized both ways).
+Glib::ustring
+FontSelector::get_fontspec(bool use_variations) {
+
+ // Build new fontspec from GUI settings
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ iter = style_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, style);
+ }
+
+ if (family.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl;
+ }
+
+ if (style.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl;
+ }
+
+ Glib::ustring fontspec = family + ", ";
+
+ if (use_variations) {
+ // Clip any font_variation data in 'style' as we'll replace it.
+ auto pos = style.find('@');
+ if (pos != Glib::ustring::npos) {
+ style.erase (pos, style.length()-1);
+ }
+
+ Glib::ustring variations = font_variations.get_pango_string();
+
+ if (variations.empty()) {
+ fontspec += style;
+ } else {
+ fontspec += variations;
+ }
+ } else {
+ fontspec += style;
+ }
+
+ return fontspec;
+}
+
+void
+FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected();
+ if (iter_family) {
+ (*iter_family).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ (*iter).get_value(1, style);
+
+ Glib::ustring style_escaped = Glib::Markup::escape_text( style );
+ Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style );
+ Glib::ustring markup;
+
+ markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>";
+
+ // std::cout << " markup: " << markup << " (" << name << ")" << std::endl;
+
+ renderer->set_property("markup", markup);
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelector::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::RefPtr<Gtk::TreeModel> model;
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model);
+
+ if (!iter) {
+ // This can happen just after the family list is recreated.
+ signal_block = false;
+ return;
+ }
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->ensureRowStyles(model, iter);
+
+ Gtk::TreeModel::Row row = *iter;
+
+ // Get family name
+ Glib::ustring family;
+ row.get_value(0, family);
+
+ // Get style list (TO DO: Get rid of GList)
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Find best style match for selected family with current style (e.g. of selected text).
+ Glib::ustring style = fontlister->get_font_style();
+ Glib::ustring best = fontlister->get_best_style_match (family, style);
+
+ // Create are own store of styles for selected font-family (the font-family selected
+ // in the dialog may not be the same as stored in the font-lister class until the
+ // "Apply" button is triggered).
+ Gtk::TreeModel::iterator it_best;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+
+ // Build list and find best match.
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (best == ((StyleNames*)styles->data)->CssName) {
+ it_best = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (it_best) {
+ style_treeview.get_selection()->select (it_best);
+ }
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_style_changed() {
+ if (signal_block) return;
+
+ // Update variations widget if new style selected from style widget.
+ signal_block = true;
+ Glib::ustring fontspec = get_fontspec( false );
+ update_variations(fontspec);
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_size_changed() {
+
+ if (signal_block) return;
+
+ double size;
+ Glib::ustring input = size_combobox.get_active_text();
+ try {
+ size = std::stod (input);
+ }
+ catch (std::invalid_argument) {
+ std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl;
+ size = -1;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // Arbitrary: Text and Font preview freezes with huge font sizes.
+ int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000);
+
+ if (size <= 0) {
+ return;
+ }
+ if (size > max_size)
+ size = max_size;
+
+ if (fabs(font_size - size) > 0.001) {
+ font_size = size;
+ // Let world know
+ changed_emit();
+ }
+}
+
+void
+FontSelector::on_variations_changed() {
+
+ if (signal_block) return;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::changed_emit() {
+ signal_block = true;
+ signal_changed.emit (get_fontspec());
+ signal_block = false;
+}
+
+void FontSelector::update_variations(const Glib::ustring& fontspec) {
+ font_variations.update(fontspec);
+
+ // Check if there are any variations available; if not, don't expand font_variations_scroll
+ bool hasContent = font_variations.variations_present();
+ font_variations_scroll.set_vexpand(hasContent);
+}
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/font-selector.h b/src/ui/widget/font-selector.h
new file mode 100644
index 0000000..137d411
--- /dev/null
+++ b/src/ui/widget/font-selector.h
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * The routines here create and manage a font selector widget with three parts,
+ * one each for font-family, font-style, and font-size.
+ *
+ * It is used by the TextEdit and Glyphs panel dialogs. The FontLister class is used
+ * to access the list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text
+ * toolbar. Fonts are kept track of by their "fontspecs" which are the same as the
+ * strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-seletor widget.
+ * Update the lists when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made so that the Text Preview can be updated.
+ * Provide the currently selected values.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_H
+#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/comboboxtext.h>
+
+#include "ui/widget/font-variations.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ *
+ * It is used by the TextEdit and Glyphs panel dialogs. The FontSelector class utilizes the
+ * FontLister class to obtain a list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts
+ * are kept track of by their "fontspecs" which are the same as the strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-selector widget.
+ * Update the child widgets when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made to a child widget.
+ */
+class FontSelector : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontSelector (bool with_size = true, bool with_variations = true);
+
+protected:
+
+ // Font family
+ Gtk::Frame family_frame;
+ Gtk::ScrolledWindow family_scroll;
+ Gtk::TreeView family_treeview;
+ Gtk::TreeViewColumn family_treecolumn;
+ Gtk::CellRendererText family_cell;
+
+ // Font style
+ Gtk::Frame style_frame;
+ Gtk::ScrolledWindow style_scroll;
+ Gtk::TreeView style_treeview;
+ Gtk::TreeViewColumn style_treecolumn;
+ Gtk::CellRendererText style_cell;
+
+ // Font size
+ Gtk::Label size_label;
+ Gtk::ComboBoxText size_combobox;
+
+ // Font variations
+ Gtk::ScrolledWindow font_variations_scroll;
+ FontVariations font_variations;
+
+private:
+
+ // Set sizes in font size combobox.
+ void set_sizes();
+ void set_fontsize_tooltip();
+
+ // Use font style when listing style names.
+ void style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter);
+
+ // Signal handlers
+ void on_family_changed();
+ void on_style_changed();
+ void on_size_changed();
+ void on_variations_changed();
+
+ // Signals
+ sigc::signal<void, Glib::ustring> signal_changed;
+ void changed_emit();
+ bool signal_block;
+
+ // Variables
+ double font_size;
+
+ // control font variations update and UI element size
+ void update_variations(const Glib::ustring& fontspec);
+
+public:
+
+ /**
+ * Update GUI based on fontspec
+ */
+ void update_font ();
+ void update_size (double size);
+
+ /**
+ * Get fontspec based on current settings. (Does not handle size, yet.)
+ */
+ Glib::ustring get_fontspec(bool use_variations = true);
+
+ /**
+ * Get font size. Could be merged with fontspec.
+ */
+ double get_fontsize() { return font_size; };
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void, Glib::ustring> slot) {
+ return signal_changed.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_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 :
diff --git a/src/ui/widget/font-variants.cpp b/src/ui/widget/font-variants.cpp
new file mode 100644
index 0000000..1087431
--- /dev/null
+++ b/src/ui/widget/font-variants.cpp
@@ -0,0 +1,1461 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2015, 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <libnrtype/font-instance.h>
+
+#include "font-variants.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ // A simple class to handle UI for one feature. We could of derived this from Gtk::HBox but by
+ // attaching widgets directly to Gtk::Grid, we keep columns lined up (which may or may not be a
+ // good thing).
+ class Feature
+ {
+ public:
+ Feature( const Glib::ustring& name, OTSubstitution& glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent)
+ : _name (name)
+ , _options (options)
+ {
+ Gtk::Label* table_name = Gtk::manage (new Gtk::Label());
+ table_name->set_markup ("\"" + name + "\" ");
+
+ grid.attach (*table_name, 0, row, 1, 1);
+
+ Gtk::FlowBox* flow_box = nullptr;
+ Gtk::ScrolledWindow* scrolled_window = nullptr;
+ if (options > 2) {
+ // If there are more than 2 option, pack them into a flowbox instead of directly putting them in the grid.
+ // Some fonts might have a table with many options (Bungee Hairline table 'ornm' has 113 entries).
+ flow_box = Gtk::manage (new Gtk::FlowBox());
+ flow_box->set_selection_mode(); // Turn off selection
+ flow_box->set_homogeneous();
+ flow_box->set_max_children_per_line (100); // Override default value
+ flow_box->set_min_children_per_line (10); // Override default value
+
+ // We pack this into a scrollbar... otherwise the minimum height is set to what is required to fit all
+ // flow box children into the flow box when the flow box has minimum width. (Crazy if you ask me!)
+ scrolled_window = Gtk::manage (new Gtk::ScrolledWindow());
+ scrolled_window->set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled_window->add(*flow_box);
+ }
+
+ Gtk::RadioButton::Group group;
+ for (int i = 0; i < options; ++i) {
+
+ // Create radio button and create or add to button group.
+ Gtk::RadioButton* button = Gtk::manage (new Gtk::RadioButton());
+ if (i == 0) {
+ group = button->get_group();
+ } else {
+ button->set_group (group);
+ }
+ button->signal_clicked().connect ( sigc::mem_fun(*parent, &FontVariants::feature_callback) );
+ buttons.push_back (button);
+
+ // Create label.
+ Gtk::Label* label = Gtk::manage (new Gtk::Label());
+
+ // Restrict label width (some fonts have lots of alternatives).
+ label->set_line_wrap( true );
+ label->set_line_wrap_mode( Pango::WRAP_WORD_CHAR );
+ label->set_ellipsize( Pango::ELLIPSIZE_END );
+ label->set_lines(3);
+ label->set_hexpand();
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += family;
+ markup += "' font_features='";
+ markup += name;
+ markup += " ";
+ markup += std::to_string (i);
+ markup += "'>";
+ markup += Glib::Markup::escape_text (glyphs.input);
+ markup += "</span>";
+ label->set_markup (markup);
+
+ // Add button and label to widget
+ if (!flow_box) {
+ // Attach directly to grid (keeps things aligned row-to-row).
+ grid.attach (*button, 2*i+1, row, 1, 1);
+ grid.attach (*label, 2*i+2, row, 1, 1);
+ } else {
+ // Pack into FlowBox
+
+ // Pack button and label into a box so they stay together.
+ Gtk::Box* box = Gtk::manage (new Gtk::Box());
+ box->add(*button);
+ box->add(*label);
+
+ flow_box->add(*box);
+ }
+ }
+
+ if (scrolled_window) {
+ grid.attach (*scrolled_window, 1, row, 4, 1);
+ }
+ }
+
+ Glib::ustring
+ get_css()
+ {
+ int i = 0;
+ for (auto b: buttons) {
+ if (b->get_active()) {
+ if (i == 0) {
+ // Features are always off by default (for those handled here).
+ return "";
+ } else if (i == 1) {
+ // Feature without value has implied value of 1.
+ return ("\"" + _name + "\", ");
+ } else {
+ // Feature with value greater than 1 must be explicitly set.
+ return ("\"" + _name + "\" " + std::to_string (i) + ", ");
+ }
+ }
+ ++i;
+ }
+ return "";
+ }
+
+ void
+ set_active(int i)
+ {
+ if (i < buttons.size()) {
+ buttons[i]->set_active();
+ }
+ }
+
+ private:
+ Glib::ustring _name;
+ int _options;
+ std::vector <Gtk::RadioButton*> buttons;
+ };
+
+ FontVariants::FontVariants () :
+ Gtk::VBox (),
+ _ligatures_frame ( Glib::ustring(C_("Font feature", "Ligatures" )) ),
+ _ligatures_common ( Glib::ustring(C_("Font feature", "Common" )) ),
+ _ligatures_discretionary ( Glib::ustring(C_("Font feature", "Discretionary")) ),
+ _ligatures_historical ( Glib::ustring(C_("Font feature", "Historical" )) ),
+ _ligatures_contextual ( Glib::ustring(C_("Font feature", "Contextual" )) ),
+
+ _position_frame ( Glib::ustring(C_("Font feature", "Position" )) ),
+ _position_normal ( Glib::ustring(C_("Font feature", "Normal" )) ),
+ _position_sub ( Glib::ustring(C_("Font feature", "Subscript" )) ),
+ _position_super ( Glib::ustring(C_("Font feature", "Superscript" )) ),
+
+ _caps_frame ( Glib::ustring(C_("Font feature", "Capitals" )) ),
+ _caps_normal ( Glib::ustring(C_("Font feature", "Normal" )) ),
+ _caps_small ( Glib::ustring(C_("Font feature", "Small" )) ),
+ _caps_all_small ( Glib::ustring(C_("Font feature", "All small" )) ),
+ _caps_petite ( Glib::ustring(C_("Font feature", "Petite" )) ),
+ _caps_all_petite ( Glib::ustring(C_("Font feature", "All petite" )) ),
+ _caps_unicase ( Glib::ustring(C_("Font feature", "Unicase" )) ),
+ _caps_titling ( Glib::ustring(C_("Font feature", "Titling" )) ),
+
+ _numeric_frame ( Glib::ustring(C_("Font feature", "Numeric" )) ),
+ _numeric_lining ( Glib::ustring(C_("Font feature", "Lining" )) ),
+ _numeric_old_style ( Glib::ustring(C_("Font feature", "Old Style" )) ),
+ _numeric_default_style ( Glib::ustring(C_("Font feature", "Default Style")) ),
+ _numeric_proportional ( Glib::ustring(C_("Font feature", "Proportional" )) ),
+ _numeric_tabular ( Glib::ustring(C_("Font feature", "Tabular" )) ),
+ _numeric_default_width ( Glib::ustring(C_("Font feature", "Default Width")) ),
+ _numeric_diagonal ( Glib::ustring(C_("Font feature", "Diagonal" )) ),
+ _numeric_stacked ( Glib::ustring(C_("Font feature", "Stacked" )) ),
+ _numeric_default_fractions( Glib::ustring(C_("Font feature", "Default Fractions")) ),
+ _numeric_ordinal ( Glib::ustring(C_("Font feature", "Ordinal" )) ),
+ _numeric_slashed_zero ( Glib::ustring(C_("Font feature", "Slashed Zero" )) ),
+
+ _asian_frame ( Glib::ustring(C_("Font feature", "East Asian" )) ),
+ _asian_default_variant ( Glib::ustring(C_("Font feature", "Default" )) ),
+ _asian_jis78 ( Glib::ustring(C_("Font feature", "JIS78" )) ),
+ _asian_jis83 ( Glib::ustring(C_("Font feature", "JIS83" )) ),
+ _asian_jis90 ( Glib::ustring(C_("Font feature", "JIS90" )) ),
+ _asian_jis04 ( Glib::ustring(C_("Font feature", "JIS04" )) ),
+ _asian_simplified ( Glib::ustring(C_("Font feature", "Simplified" )) ),
+ _asian_traditional ( Glib::ustring(C_("Font feature", "Traditional" )) ),
+ _asian_default_width ( Glib::ustring(C_("Font feature", "Default" )) ),
+ _asian_full_width ( Glib::ustring(C_("Font feature", "Full Width" )) ),
+ _asian_proportional_width ( Glib::ustring(C_("Font feature", "Proportional" )) ),
+ _asian_ruby ( Glib::ustring(C_("Font feature", "Ruby" )) ),
+
+ _feature_frame ( Glib::ustring(C_("Font feature", "Feature Settings")) ),
+ _feature_label ( Glib::ustring(C_("Font feature", "Selection has different Feature Settings!")) ),
+
+ _ligatures_changed( false ),
+ _position_changed( false ),
+ _caps_changed( false ),
+ _numeric_changed( false ),
+ _asian_changed( false )
+
+ {
+
+ set_name ( "FontVariants" );
+
+ // Ligatures --------------------------
+
+ // Add tooltips
+ _ligatures_common.set_tooltip_text(
+ _("Common ligatures. On by default. OpenType tables: 'liga', 'clig'"));
+ _ligatures_discretionary.set_tooltip_text(
+ _("Discretionary ligatures. Off by default. OpenType table: 'dlig'"));
+ _ligatures_historical.set_tooltip_text(
+ _("Historical ligatures. Off by default. OpenType table: 'hlig'"));
+ _ligatures_contextual.set_tooltip_text(
+ _("Contextual forms. On by default. OpenType table: 'calt'"));
+
+ // Add signals
+ _ligatures_common.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_discretionary.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_historical.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_contextual.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+
+ // Restrict label widths (some fonts have lots of ligatures). Must also set ellipsize mode.
+ _ligatures_label_common.set_max_width_chars( 60 );
+ _ligatures_label_discretionary.set_max_width_chars( 60 );
+ _ligatures_label_historical.set_max_width_chars( 60 );
+ _ligatures_label_contextual.set_max_width_chars( 60 );
+
+ _ligatures_label_common.set_ellipsize( Pango::ELLIPSIZE_END );
+ _ligatures_label_discretionary.set_ellipsize( Pango::ELLIPSIZE_END );
+ _ligatures_label_historical.set_ellipsize( Pango::ELLIPSIZE_END );
+ _ligatures_label_contextual.set_ellipsize( Pango::ELLIPSIZE_END );
+
+ _ligatures_label_common.set_lines( 5 );
+ _ligatures_label_discretionary.set_lines( 5 );
+ _ligatures_label_historical.set_lines( 5 );
+ _ligatures_label_contextual.set_lines( 5 );
+
+ // Allow user to select characters. Not useful as this selects the ligatures.
+ // _ligatures_label_common.set_selectable( true );
+ // _ligatures_label_discretionary.set_selectable( true );
+ // _ligatures_label_historical.set_selectable( true );
+ // _ligatures_label_contextual.set_selectable( true );
+
+ // Add to frame
+ _ligatures_grid.attach( _ligatures_common, 0, 0, 1, 1);
+ _ligatures_grid.attach( _ligatures_discretionary, 0, 1, 1, 1);
+ _ligatures_grid.attach( _ligatures_historical, 0, 2, 1, 1);
+ _ligatures_grid.attach( _ligatures_contextual, 0, 3, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_common, 1, 0, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_discretionary, 1, 1, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_historical, 1, 2, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_contextual, 1, 3, 1, 1);
+
+ _ligatures_grid.set_margin_start(15);
+ _ligatures_grid.set_margin_end(15);
+
+ _ligatures_frame.add( _ligatures_grid );
+ pack_start( _ligatures_frame, Gtk::PACK_SHRINK );
+
+ ligatures_init();
+
+ // Position ----------------------------------
+
+ // Add tooltips
+ _position_normal.set_tooltip_text( _("Normal position."));
+ _position_sub.set_tooltip_text( _("Subscript. OpenType table: 'subs'") );
+ _position_super.set_tooltip_text( _("Superscript. OpenType table: 'sups'") );
+
+ // Group buttons
+ Gtk::RadioButton::Group position_group = _position_normal.get_group();
+ _position_sub.set_group(position_group);
+ _position_super.set_group(position_group);
+
+ // Add signals
+ _position_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+ _position_sub.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+ _position_super.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+
+ // Add to frame
+ _position_grid.attach( _position_normal, 0, 0, 1, 1);
+ _position_grid.attach( _position_sub, 1, 0, 1, 1);
+ _position_grid.attach( _position_super, 2, 0, 1, 1);
+
+ _position_grid.set_margin_start(15);
+ _position_grid.set_margin_end(15);
+
+ _position_frame.add( _position_grid );
+ pack_start( _position_frame, Gtk::PACK_SHRINK );
+
+ position_init();
+
+ // Caps ----------------------------------
+
+ // Add tooltips
+ _caps_normal.set_tooltip_text( _("Normal capitalization."));
+ _caps_small.set_tooltip_text( _("Small-caps (lowercase). OpenType table: 'smcp'"));
+ _caps_all_small.set_tooltip_text( _("All small-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'smcp'"));
+ _caps_petite.set_tooltip_text( _("Petite-caps (lowercase). OpenType table: 'pcap'"));
+ _caps_all_petite.set_tooltip_text( _("All petite-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'pcap'"));
+ _caps_unicase.set_tooltip_text( _("Unicase (small caps for uppercase, normal for lowercase). OpenType table: 'unic'"));
+ _caps_titling.set_tooltip_text( _("Titling caps (lighter-weight uppercase for use in titles). OpenType table: 'titl'"));
+
+ // Group buttons
+ Gtk::RadioButton::Group caps_group = _caps_normal.get_group();
+ _caps_small.set_group(caps_group);
+ _caps_all_small.set_group(caps_group);
+ _caps_petite.set_group(caps_group);
+ _caps_all_petite.set_group(caps_group);
+ _caps_unicase.set_group(caps_group);
+ _caps_titling.set_group(caps_group);
+
+ // Add signals
+ _caps_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_all_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_all_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_unicase.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_titling.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+
+ // Add to frame
+ _caps_grid.attach( _caps_normal, 0, 0, 1, 1);
+ _caps_grid.attach( _caps_unicase, 1, 0, 1, 1);
+ _caps_grid.attach( _caps_titling, 2, 0, 1, 1);
+ _caps_grid.attach( _caps_small, 0, 1, 1, 1);
+ _caps_grid.attach( _caps_all_small, 1, 1, 1, 1);
+ _caps_grid.attach( _caps_petite, 2, 1, 1, 1);
+ _caps_grid.attach( _caps_all_petite, 3, 1, 1, 1);
+
+ _caps_grid.set_margin_start(15);
+ _caps_grid.set_margin_end(15);
+
+ _caps_frame.add( _caps_grid );
+ pack_start( _caps_frame, Gtk::PACK_SHRINK );
+
+ caps_init();
+
+ // Numeric ------------------------------
+
+ // Add tooltips
+ _numeric_default_style.set_tooltip_text( _("Normal style."));
+ _numeric_lining.set_tooltip_text( _("Lining numerals. OpenType table: 'lnum'"));
+ _numeric_old_style.set_tooltip_text( _("Old style numerals. OpenType table: 'onum'"));
+ _numeric_default_width.set_tooltip_text( _("Normal widths."));
+ _numeric_proportional.set_tooltip_text( _("Proportional width numerals. OpenType table: 'pnum'"));
+ _numeric_tabular.set_tooltip_text( _("Same width numerals. OpenType table: 'tnum'"));
+ _numeric_default_fractions.set_tooltip_text( _("Normal fractions."));
+ _numeric_diagonal.set_tooltip_text( _("Diagonal fractions. OpenType table: 'frac'"));
+ _numeric_stacked.set_tooltip_text( _("Stacked fractions. OpenType table: 'afrc'"));
+ _numeric_ordinal.set_tooltip_text( _("Ordinals (raised 'th', etc.). OpenType table: 'ordn'"));
+ _numeric_slashed_zero.set_tooltip_text( _("Slashed zeros. OpenType table: 'zero'"));
+
+ // Group buttons
+ Gtk::RadioButton::Group style_group = _numeric_default_style.get_group();
+ _numeric_lining.set_group(style_group);
+ _numeric_old_style.set_group(style_group);
+
+ Gtk::RadioButton::Group width_group = _numeric_default_width.get_group();
+ _numeric_proportional.set_group(width_group);
+ _numeric_tabular.set_group(width_group);
+
+ Gtk::RadioButton::Group fraction_group = _numeric_default_fractions.get_group();
+ _numeric_diagonal.set_group(fraction_group);
+ _numeric_stacked.set_group(fraction_group);
+
+ // Add signals
+ _numeric_default_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_lining.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_old_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_proportional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_tabular.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_default_fractions.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_diagonal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_stacked.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_ordinal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_slashed_zero.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+
+ // Add to frame
+ _numeric_grid.attach (_numeric_default_style, 0, 0, 1, 1);
+ _numeric_grid.attach (_numeric_lining, 1, 0, 1, 1);
+ _numeric_grid.attach (_numeric_lining_label, 2, 0, 1, 1);
+ _numeric_grid.attach (_numeric_old_style, 3, 0, 1, 1);
+ _numeric_grid.attach (_numeric_old_style_label, 4, 0, 1, 1);
+
+ _numeric_grid.attach (_numeric_default_width, 0, 1, 1, 1);
+ _numeric_grid.attach (_numeric_proportional, 1, 1, 1, 1);
+ _numeric_grid.attach (_numeric_proportional_label, 2, 1, 1, 1);
+ _numeric_grid.attach (_numeric_tabular, 3, 1, 1, 1);
+ _numeric_grid.attach (_numeric_tabular_label, 4, 1, 1, 1);
+
+ _numeric_grid.attach (_numeric_default_fractions, 0, 2, 1, 1);
+ _numeric_grid.attach (_numeric_diagonal, 1, 2, 1, 1);
+ _numeric_grid.attach (_numeric_diagonal_label, 2, 2, 1, 1);
+ _numeric_grid.attach (_numeric_stacked, 3, 2, 1, 1);
+ _numeric_grid.attach (_numeric_stacked_label, 4, 2, 1, 1);
+
+ _numeric_grid.attach (_numeric_ordinal, 0, 3, 1, 1);
+ _numeric_grid.attach (_numeric_ordinal_label, 1, 3, 4, 1);
+
+ _numeric_grid.attach (_numeric_slashed_zero, 0, 4, 1, 1);
+ _numeric_grid.attach (_numeric_slashed_zero_label, 1, 4, 1, 1);
+
+ _numeric_grid.set_margin_start(15);
+ _numeric_grid.set_margin_end(15);
+
+ _numeric_frame.add( _numeric_grid );
+ pack_start( _numeric_frame, Gtk::PACK_SHRINK );
+
+ // East Asian
+
+ // Add tooltips
+ _asian_default_variant.set_tooltip_text ( _("Default variant."));
+ _asian_jis78.set_tooltip_text( _("JIS78 forms. OpenType table: 'jp78'."));
+ _asian_jis83.set_tooltip_text( _("JIS83 forms. OpenType table: 'jp83'."));
+ _asian_jis90.set_tooltip_text( _("JIS90 forms. OpenType table: 'jp90'."));
+ _asian_jis04.set_tooltip_text( _("JIS2004 forms. OpenType table: 'jp04'."));
+ _asian_simplified.set_tooltip_text( _("Simplified forms. OpenType table: 'smpl'."));
+ _asian_traditional.set_tooltip_text( _("Traditional forms. OpenType table: 'trad'."));
+ _asian_default_width.set_tooltip_text ( _("Default width."));
+ _asian_full_width.set_tooltip_text( _("Full width variants. OpenType table: 'fwid'."));
+ _asian_proportional_width.set_tooltip_text(_("Proportional width variants. OpenType table: 'pwid'."));
+ _asian_ruby.set_tooltip_text( _("Ruby variants. OpenType table: 'ruby'."));
+
+ // Add signals
+ _asian_default_variant.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis78.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis83.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis90.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis04.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_simplified.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_traditional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_full_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_proportional_width.signal_clicked().connect (sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_ruby.signal_clicked().connect( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+
+ // Add to frame
+ _asian_grid.attach (_asian_default_variant, 0, 0, 1, 1);
+ _asian_grid.attach (_asian_jis78, 1, 0, 1, 1);
+ _asian_grid.attach (_asian_jis83, 2, 0, 1, 1);
+ _asian_grid.attach (_asian_jis90, 1, 1, 1, 1);
+ _asian_grid.attach (_asian_jis04, 2, 1, 1, 1);
+ _asian_grid.attach (_asian_simplified, 1, 2, 1, 1);
+ _asian_grid.attach (_asian_traditional, 2, 2, 1, 1);
+ _asian_grid.attach (_asian_default_width, 0, 3, 1, 1);
+ _asian_grid.attach (_asian_full_width, 1, 3, 1, 1);
+ _asian_grid.attach (_asian_proportional_width, 2, 3, 1, 1);
+ _asian_grid.attach (_asian_ruby, 0, 4, 1, 1);
+
+ _asian_grid.set_margin_start(15);
+ _asian_grid.set_margin_end(15);
+
+ _asian_frame.add( _asian_grid );
+ pack_start( _asian_frame, Gtk::PACK_SHRINK );
+
+ // Group Buttons
+ Gtk::RadioButton::Group asian_variant_group = _asian_default_variant.get_group();
+ _asian_jis78.set_group(asian_variant_group);
+ _asian_jis83.set_group(asian_variant_group);
+ _asian_jis90.set_group(asian_variant_group);
+ _asian_jis04.set_group(asian_variant_group);
+ _asian_simplified.set_group(asian_variant_group);
+ _asian_traditional.set_group(asian_variant_group);
+
+ Gtk::RadioButton::Group asian_width_group = _asian_default_width.get_group();
+ _asian_full_width.set_group (asian_width_group);
+ _asian_proportional_width.set_group (asian_width_group);
+
+ // Feature settings ---------------------
+
+ // Add tooltips
+ _feature_entry.set_tooltip_text( _("Feature settings in CSS form (e.g. \"wxyz\" or \"wxyz\" 3)."));
+
+ _feature_substitutions.set_justify( Gtk::JUSTIFY_LEFT );
+ _feature_substitutions.set_line_wrap( true );
+ _feature_substitutions.set_line_wrap_mode( Pango::WRAP_WORD_CHAR );
+
+ _feature_list.set_justify( Gtk::JUSTIFY_LEFT );
+ _feature_list.set_line_wrap( true );
+
+ // Add to frame
+ _feature_vbox.pack_start( _feature_grid, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_entry, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_label, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_substitutions, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_list, Gtk::PACK_SHRINK );
+
+ _feature_vbox.set_margin_start(15);
+ _feature_vbox.set_margin_end(15);
+
+ _feature_frame.add( _feature_vbox );
+ pack_start( _feature_frame, Gtk::PACK_SHRINK );
+
+ // Add signals
+ //_feature_entry.signal_key_press_event().connect ( sigc::mem_fun(*this, &FontVariants::feature_callback) );
+ _feature_entry.signal_changed().connect( sigc::mem_fun(*this, &FontVariants::feature_callback) );
+
+ show_all_children();
+
+ }
+
+ void
+ FontVariants::ligatures_init() {
+ // std::cout << "FontVariants::ligatures_init()" << std::endl;
+ }
+
+ void
+ FontVariants::ligatures_callback() {
+ // std::cout << "FontVariants::ligatures_callback()" << std::endl;
+ _ligatures_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::position_init() {
+ // std::cout << "FontVariants::position_init()" << std::endl;
+ }
+
+ void
+ FontVariants::position_callback() {
+ // std::cout << "FontVariants::position_callback()" << std::endl;
+ _position_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::caps_init() {
+ // std::cout << "FontVariants::caps_init()" << std::endl;
+ }
+
+ void
+ FontVariants::caps_callback() {
+ // std::cout << "FontVariants::caps_callback()" << std::endl;
+ _caps_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::numeric_init() {
+ // std::cout << "FontVariants::numeric_init()" << std::endl;
+ }
+
+ void
+ FontVariants::numeric_callback() {
+ // std::cout << "FontVariants::numeric_callback()" << std::endl;
+ _numeric_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::asian_init() {
+ // std::cout << "FontVariants::asian_init()" << std::endl;
+ }
+
+ void
+ FontVariants::asian_callback() {
+ // std::cout << "FontVariants::asian_callback()" << std::endl;
+ _asian_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::feature_init() {
+ // std::cout << "FontVariants::feature_init()" << std::endl;
+ }
+
+ void
+ FontVariants::feature_callback() {
+ // std::cout << "FontVariants::feature_callback()" << std::endl;
+ _feature_changed = true;
+ _changed_signal.emit();
+ }
+
+ // Update GUI based on query.
+ void
+ FontVariants::update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ) {
+
+ update_opentype( font_spec );
+
+ _ligatures_all = query->font_variant_ligatures.computed;
+ _ligatures_mix = query->font_variant_ligatures.value;
+
+ _ligatures_common.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_COMMON );
+ _ligatures_discretionary.set_active(_ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY );
+ _ligatures_historical.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL );
+ _ligatures_contextual.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL );
+
+ _ligatures_common.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_COMMON );
+ _ligatures_discretionary.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY );
+ _ligatures_historical.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL );
+ _ligatures_contextual.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL );
+
+ _position_all = query->font_variant_position.computed;
+ _position_mix = query->font_variant_position.value;
+
+ _position_normal.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_NORMAL );
+ _position_sub.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUB );
+ _position_super.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUPER );
+
+ _position_normal.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_NORMAL );
+ _position_sub.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUB );
+ _position_super.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUPER );
+
+ _caps_all = query->font_variant_caps.computed;
+ _caps_mix = query->font_variant_caps.value;
+
+ _caps_normal.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_NORMAL );
+ _caps_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_SMALL );
+ _caps_all_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL );
+ _caps_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_PETITE );
+ _caps_all_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE );
+ _caps_unicase.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_UNICASE );
+ _caps_titling.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_TITLING );
+
+ _caps_normal.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_NORMAL );
+ _caps_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_SMALL );
+ _caps_all_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL );
+ _caps_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_PETITE );
+ _caps_all_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE );
+ _caps_unicase.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_UNICASE );
+ _caps_titling.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_TITLING );
+
+ _numeric_all = query->font_variant_numeric.computed;
+ _numeric_mix = query->font_variant_numeric.value;
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS) {
+ _numeric_lining.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS) {
+ _numeric_old_style.set_active();
+ } else {
+ _numeric_default_style.set_active();
+ }
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS) {
+ _numeric_proportional.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS) {
+ _numeric_tabular.set_active();
+ } else {
+ _numeric_default_width.set_active();
+ }
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS) {
+ _numeric_diagonal.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS) {
+ _numeric_stacked.set_active();
+ } else {
+ _numeric_default_fractions.set_active();
+ }
+
+ _numeric_ordinal.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL );
+ _numeric_slashed_zero.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO );
+
+
+ _numeric_lining.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS );
+ _numeric_old_style.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS );
+ _numeric_proportional.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS );
+ _numeric_tabular.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS );
+ _numeric_diagonal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS );
+ _numeric_stacked.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS );
+ _numeric_ordinal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL );
+ _numeric_slashed_zero.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO );
+
+ _asian_all = query->font_variant_east_asian.computed;
+ _asian_mix = query->font_variant_east_asian.value;
+
+ if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78) {
+ _asian_jis78.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83) {
+ _asian_jis83.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90) {
+ _asian_jis90.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04) {
+ _asian_jis04.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED) {
+ _asian_simplified.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL) {
+ _asian_traditional.set_active();
+ } else {
+ _asian_default_variant.set_active();
+ }
+
+ if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH) {
+ _asian_full_width.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH) {
+ _asian_proportional_width.set_active();
+ } else {
+ _asian_default_width.set_active();
+ }
+
+ _asian_ruby.set_active ( _asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY );
+
+ _asian_jis78.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78);
+ _asian_jis83.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83);
+ _asian_jis90.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90);
+ _asian_jis04.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04);
+ _asian_simplified.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED);
+ _asian_traditional.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL);
+ _asian_full_width.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH);
+ _asian_proportional_width.set_inconsistent(_asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH);
+ _asian_ruby.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY);
+
+ // Fix me: Should match a space if second part matches. ---,
+ // : Add boundary to 'on' and 'off'. v
+ Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("\"(\\w{4})\"\\s*([0-9]+|on|off|)");
+ Glib::MatchInfo matchInfo;
+ std::string setting;
+
+ // Set feature radiobutton (if it exists) or add to _feature_entry string.
+ char const *val = query->font_feature_settings.value();
+ if (val) {
+
+ std::vector<Glib::ustring> tokens =
+ Glib::Regex::split_simple("\\s*,\\s*", val);
+
+ for (auto token: tokens) {
+ regex->match(token, matchInfo);
+ if (matchInfo.matches()) {
+ Glib::ustring table = matchInfo.fetch(1);
+ Glib::ustring value = matchInfo.fetch(2);
+
+ if (_features.find(table) != _features.end()) {
+ int v = 0;
+ if (value == "0" || value == "off") v = 0;
+ else if (value == "1" || value == "on" || value.empty() ) v = 1;
+ else v = std::stoi(value);
+ _features[table]->set_active(v);
+ } else {
+ setting += token + ", ";
+ }
+ }
+ }
+ }
+
+ // Remove final ", "
+ if (setting.length() > 1) {
+ setting.pop_back();
+ setting.pop_back();
+ }
+
+ // Tables without radiobuttons.
+ _feature_entry.set_text( setting );
+
+ if( different_features ) {
+ _feature_label.show();
+ } else {
+ _feature_label.hide();
+ }
+ }
+
+ // Update GUI based on OpenType tables of selected font (which may be changed in font selector tab).
+ void
+ FontVariants::update_opentype (Glib::ustring& font_spec) {
+
+ // Disable/Enable based on available OpenType tables.
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification( font_spec.c_str() );
+ if( res ) {
+
+ std::map<Glib::ustring, OTSubstitution>::iterator it;
+
+ if((it = res->openTypeTables.find("liga"))!= res->openTypeTables.end() ||
+ (it = res->openTypeTables.find("clig"))!= res->openTypeTables.end()) {
+ _ligatures_common.set_sensitive();
+ } else {
+ _ligatures_common.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("dlig"))!= res->openTypeTables.end()) {
+ _ligatures_discretionary.set_sensitive();
+ } else {
+ _ligatures_discretionary.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("hlig"))!= res->openTypeTables.end()) {
+ _ligatures_historical.set_sensitive();
+ } else {
+ _ligatures_historical.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("calt"))!= res->openTypeTables.end()) {
+ _ligatures_contextual.set_sensitive();
+ } else {
+ _ligatures_contextual.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("subs"))!= res->openTypeTables.end()) {
+ _position_sub.set_sensitive();
+ } else {
+ _position_sub.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("sups"))!= res->openTypeTables.end()) {
+ _position_super.set_sensitive();
+ } else {
+ _position_super.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_small.set_sensitive();
+ } else {
+ _caps_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_all_small.set_sensitive();
+ } else {
+ _caps_all_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_petite.set_sensitive();
+ } else {
+ _caps_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_all_petite.set_sensitive();
+ } else {
+ _caps_all_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("unic"))!= res->openTypeTables.end()) {
+ _caps_unicase.set_sensitive();
+ } else {
+ _caps_unicase.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("titl"))!= res->openTypeTables.end()) {
+ _caps_titling.set_sensitive();
+ } else {
+ _caps_titling.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("lnum"))!= res->openTypeTables.end()) {
+ _numeric_lining.set_sensitive();
+ } else {
+ _numeric_lining.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("onum"))!= res->openTypeTables.end()) {
+ _numeric_old_style.set_sensitive();
+ } else {
+ _numeric_old_style.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pnum"))!= res->openTypeTables.end()) {
+ _numeric_proportional.set_sensitive();
+ } else {
+ _numeric_proportional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("tnum"))!= res->openTypeTables.end()) {
+ _numeric_tabular.set_sensitive();
+ } else {
+ _numeric_tabular.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("frac"))!= res->openTypeTables.end()) {
+ _numeric_diagonal.set_sensitive();
+ } else {
+ _numeric_diagonal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("afrac"))!= res->openTypeTables.end()) {
+ _numeric_stacked.set_sensitive();
+ } else {
+ _numeric_stacked.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ordn"))!= res->openTypeTables.end()) {
+ _numeric_ordinal.set_sensitive();
+ } else {
+ _numeric_ordinal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("zero"))!= res->openTypeTables.end()) {
+ _numeric_slashed_zero.set_sensitive();
+ } else {
+ _numeric_slashed_zero.set_sensitive( false );
+ }
+
+ // East-Asian
+ if((it = res->openTypeTables.find("jp78"))!= res->openTypeTables.end()) {
+ _asian_jis78.set_sensitive();
+ } else {
+ _asian_jis78.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp83"))!= res->openTypeTables.end()) {
+ _asian_jis83.set_sensitive();
+ } else {
+ _asian_jis83.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp90"))!= res->openTypeTables.end()) {
+ _asian_jis90.set_sensitive();
+ } else {
+ _asian_jis90.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp04"))!= res->openTypeTables.end()) {
+ _asian_jis04.set_sensitive();
+ } else {
+ _asian_jis04.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smpl"))!= res->openTypeTables.end()) {
+ _asian_simplified.set_sensitive();
+ } else {
+ _asian_simplified.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("trad"))!= res->openTypeTables.end()) {
+ _asian_traditional.set_sensitive();
+ } else {
+ _asian_traditional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("fwid"))!= res->openTypeTables.end()) {
+ _asian_full_width.set_sensitive();
+ } else {
+ _asian_full_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pwid"))!= res->openTypeTables.end()) {
+ _asian_proportional_width.set_sensitive();
+ } else {
+ _asian_proportional_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ruby"))!= res->openTypeTables.end()) {
+ _asian_ruby.set_sensitive();
+ } else {
+ _asian_ruby.set_sensitive( false );
+ }
+
+ // List available ligatures
+ Glib::ustring markup_liga;
+ Glib::ustring markup_dlig;
+ Glib::ustring markup_hlig;
+ Glib::ustring markup_calt;
+
+ for (auto table: res->openTypeTables) {
+
+ if (table.first == "liga" ||
+ table.first == "clig" ||
+ table.first == "dlig" ||
+ table.first == "hgli" ||
+ table.first == "calt") {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "'>";
+ markup += Glib::Markup::escape_text(table.second.output);
+ markup += "</span>";
+
+ if (table.first == "liga") markup_liga += markup;
+ if (table.first == "clig") markup_liga += markup;
+ if (table.first == "dlig") markup_dlig += markup;
+ if (table.first == "hlig") markup_hlig += markup;
+ if (table.first == "calt") markup_calt += markup;
+ }
+ }
+
+ _ligatures_label_common.set_markup ( markup_liga.c_str() );
+ _ligatures_label_discretionary.set_markup ( markup_dlig.c_str() );
+ _ligatures_label_historical.set_markup ( markup_hlig.c_str() );
+ _ligatures_label_contextual.set_markup ( markup_calt.c_str() );
+
+ // List available numeric variants
+ Glib::ustring markup_lnum;
+ Glib::ustring markup_onum;
+ Glib::ustring markup_pnum;
+ Glib::ustring markup_tnum;
+ Glib::ustring markup_frac;
+ Glib::ustring markup_afrc;
+ Glib::ustring markup_ordn;
+ Glib::ustring markup_zero;
+
+ for (auto table: res->openTypeTables) {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "' font_features='";
+ markup += table.first;
+ markup += "'>";
+ if (table.first == "lnum" ||
+ table.first == "onum" ||
+ table.first == "pnum" ||
+ table.first == "tnum") markup += "0123456789";
+ if (table.first == "zero") markup += "0";
+ if (table.first == "ordn") markup += "[" + table.second.before + "]" + table.second.output;
+ if (table.first == "frac" ||
+ table.first == "afrc" ) markup += "1/2 2/3 3/4 4/5 5/6"; // Can we do better?
+ markup += "</span>";
+
+ if (table.first == "lnum") markup_lnum += markup;
+ if (table.first == "onum") markup_onum += markup;
+ if (table.first == "pnum") markup_pnum += markup;
+ if (table.first == "tnum") markup_tnum += markup;
+ if (table.first == "frac") markup_frac += markup;
+ if (table.first == "afrc") markup_afrc += markup;
+ if (table.first == "ordn") markup_ordn += markup;
+ if (table.first == "zero") markup_zero += markup;
+ }
+
+ _numeric_lining_label.set_markup ( markup_lnum.c_str() );
+ _numeric_old_style_label.set_markup ( markup_onum.c_str() );
+ _numeric_proportional_label.set_markup ( markup_pnum.c_str() );
+ _numeric_tabular_label.set_markup ( markup_tnum.c_str() );
+ _numeric_diagonal_label.set_markup ( markup_frac.c_str() );
+ _numeric_stacked_label.set_markup ( markup_afrc.c_str() );
+ _numeric_ordinal_label.set_markup ( markup_ordn.c_str() );
+ _numeric_slashed_zero_label.set_markup ( markup_zero.c_str() );
+
+ // Make list of tables not handled above.
+ std::map<Glib::ustring, OTSubstitution> table_copy = res->openTypeTables;
+ if( (it = table_copy.find("liga")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("clig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("hlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("calt")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("subs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("sups")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("smcp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2sc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pcap")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2pc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("unic")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("titl")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("lnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("onum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("tnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("frac")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("afrc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ordn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("zero")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("jp78")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp83")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp90")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp04")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("smpl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("trad")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ruby")) != table_copy.end() ) table_copy.erase( it );
+
+ // An incomplete list of tables that should not be exposed to the user:
+ if( (it = table_copy.find("abvf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("abvs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("akhn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blwf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blws")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ccmp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("cjct")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dnom")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dtls")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fina")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("half")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("haln")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("init")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("isol")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("locl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("medi")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("nukt")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("numr")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pref")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pres")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pstf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("psts")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rkrf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rphf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rtlm")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ssty")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("vatu")) != table_copy.end() ) table_copy.erase( it );
+
+ // Clear out old features
+ auto children = _feature_grid.get_children();
+ for (auto child: children) {
+ _feature_grid.remove (*child);
+ }
+ _features.clear();
+
+ std::string markup;
+ int grid_row = 0;
+
+ // GSUB lookup type 1 (1 to 1 mapping).
+ for (auto table: res->openTypeTables) {
+ if (table.first == "case" ||
+ table.first == "hist" ||
+ (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't'))) {
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ _features[table.first] = new Feature (table.first, table.second, 2,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ // GSUB lookup type 3 (1 to many mapping). Optionally type 1.
+ for (auto table: res->openTypeTables) {
+ if (table.first == "salt" ||
+ table.first == "swsh" ||
+ table.first == "cwsh" ||
+ table.first == "ornm" ||
+ table.first == "nalt" ||
+ (table.first[0] == 'c' && table.first[1] == 'v')) {
+
+ if (table.second.input.length() == 0) {
+ // This can happen if a table is not in the 'DFLT' script and 'dflt' language.
+ // We should be using the 'lang' attribute to find the correct tables.
+ // std::cerr << "FontVariants::open_type_update: "
+ // << table.first << " has no entries!" << std::endl;
+ continue;
+ }
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ // Our lame attempt at determining number of alternative glyphs for one glyph:
+ int number = table.second.output.length() / table.second.input.length();
+ if (number < 1) {
+ number = 1; // Must have at least on/off, see comment above about 'lang' attribute.
+ // std::cout << table.first << " "
+ // << table.second.output.length() << "/"
+ // << table.second.input.length() << "="
+ // << number << std::endl;
+ }
+
+ _features[table.first] = new Feature (table.first, table.second, number+1,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ _feature_grid.show_all();
+
+ _feature_substitutions.set_markup ( markup.c_str() );
+
+ std::string ott_list = "OpenType tables not included above: ";
+ for(it = table_copy.begin(); it != table_copy.end(); ++it) {
+ ott_list += it->first;
+ ott_list += ", ";
+ }
+
+ if (table_copy.size() > 0) {
+ ott_list.pop_back();
+ ott_list.pop_back();
+ _feature_list.set_text( ott_list.c_str() );
+ } else {
+ _feature_list.set_text( "" );
+ }
+
+ } else {
+ std::cerr << "FontVariants::update(): Couldn't find font_instance for: "
+ << font_spec << std::endl;
+ }
+
+ _ligatures_changed = false;
+ _position_changed = false;
+ _caps_changed = false;
+ _numeric_changed = false;
+ _feature_changed = false;
+ }
+
+ void
+ FontVariants::fill_css( SPCSSAttr *css ) {
+
+ // Ligatures
+ bool common = _ligatures_common.get_active();
+ bool discretionary = _ligatures_discretionary.get_active();
+ bool historical = _ligatures_historical.get_active();
+ bool contextual = _ligatures_contextual.get_active();
+
+ if( !common && !discretionary && !historical && !contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "none" );
+ } else if ( common && !discretionary && !historical && contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "normal" );
+ } else {
+ Glib::ustring css_string;
+ if ( !common )
+ css_string += "no-common-ligatures ";
+ if ( discretionary )
+ css_string += "discretionary-ligatures ";
+ if ( historical )
+ css_string += "historical-ligatures ";
+ if ( !contextual )
+ css_string += "no-contextual ";
+ sp_repr_css_set_property(css, "font-variant-ligatures", css_string.c_str() );
+ }
+
+ // Position
+ {
+ unsigned position_new = SP_CSS_FONT_VARIANT_POSITION_NORMAL;
+ Glib::ustring css_string;
+ if( _position_normal.get_active() ) {
+ css_string = "normal";
+ } else if( _position_sub.get_active() ) {
+ css_string = "sub";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUB;
+ } else if( _position_super.get_active() ) {
+ css_string = "super";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUPER;
+ }
+
+ // 'if' may not be necessary... need to test.
+ if( (_position_all != position_new) || ((_position_mix != 0) && _position_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-position", css_string.c_str() );
+ }
+ }
+
+ // Caps
+ {
+ //unsigned caps_new;
+ Glib::ustring css_string;
+ if( _caps_normal.get_active() ) {
+ css_string = "normal";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ } else if( _caps_small.get_active() ) {
+ css_string = "small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_SMALL;
+ } else if( _caps_all_small.get_active() ) {
+ css_string = "all-small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL;
+ } else if( _caps_petite.get_active() ) {
+ css_string = "petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_PETITE;
+ } else if( _caps_all_petite.get_active() ) {
+ css_string = "all-petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE;
+ } else if( _caps_unicase.get_active() ) {
+ css_string = "unicase";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_UNICASE;
+ } else if( _caps_titling.get_active() ) {
+ css_string = "titling";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_TITLING;
+ //} else {
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ }
+
+ // May not be necessary... need to test.
+ //if( (_caps_all != caps_new) || ((_caps_mix != 0) && _caps_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-caps", css_string.c_str() );
+ //}
+ }
+
+ // Numeric
+ bool default_style = _numeric_default_style.get_active();
+ bool lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool default_width = _numeric_default_width.get_active();
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.get_active();
+
+ bool default_fractions = _numeric_default_fractions.get_active();
+ bool diagonal = _numeric_diagonal.get_active();
+ bool stacked = _numeric_stacked.get_active();
+
+ bool ordinal = _numeric_ordinal.get_active();
+ bool slashed_zero = _numeric_slashed_zero.get_active();
+
+ if (default_style & default_width & default_fractions & !ordinal & !slashed_zero) {
+ sp_repr_css_set_property(css, "font-variant-numeric", "normal");
+ } else {
+ Glib::ustring css_string;
+ if ( lining )
+ css_string += "lining-nums ";
+ if ( old_style )
+ css_string += "oldstyle-nums ";
+ if ( proportional )
+ css_string += "proportional-nums ";
+ if ( tabular )
+ css_string += "tabular-nums ";
+ if ( diagonal )
+ css_string += "diagonal-fractions ";
+ if ( stacked )
+ css_string += "stacked-fractions ";
+ if ( ordinal )
+ css_string += "ordinal ";
+ if ( slashed_zero )
+ css_string += "slashed-zero ";
+ sp_repr_css_set_property(css, "font-variant-numeric", css_string.c_str() );
+ }
+
+ // East Asian
+ bool default_variant = _asian_default_variant.get_active();
+ bool jis78 = _asian_jis78.get_active();
+ bool jis83 = _asian_jis83.get_active();
+ bool jis90 = _asian_jis90.get_active();
+ bool jis04 = _asian_jis04.get_active();
+ bool simplified = _asian_simplified.get_active();
+ bool traditional = _asian_traditional.get_active();
+ bool asian_width = _asian_default_width.get_active();
+ bool fwid = _asian_full_width.get_active();
+ bool pwid = _asian_proportional_width.get_active();
+ bool ruby = _asian_ruby.get_active();
+
+ if (default_style & asian_width & !ruby) {
+ sp_repr_css_set_property(css, "font-variant-east-asian", "normal");
+ } else {
+ Glib::ustring css_string;
+ if (jis78) css_string += "jis78 ";
+ if (jis83) css_string += "jis83 ";
+ if (jis90) css_string += "jis90 ";
+ if (jis04) css_string += "jis04 ";
+ if (simplified) css_string += "simplfied ";
+ if (traditional) css_string += "traditional ";
+
+ if (fwid) css_string += "fwid ";
+ if (pwid) css_string += "pwid ";
+
+ if (ruby) css_string += "ruby ";
+
+ sp_repr_css_set_property(css, "font-variant-east-asian", css_string.c_str() );
+ }
+
+ // Feature settings
+ Glib::ustring feature_string;
+ for (auto i: _features) {
+ feature_string += i.second->get_css();
+ }
+
+ feature_string += _feature_entry.get_text();
+ // std::cout << "feature_string: " << feature_string << std::endl;
+
+ if (!feature_string.empty()) {
+ sp_repr_css_set_property(css, "font-feature-settings", feature_string.c_str());
+ } else {
+ sp_repr_css_unset_property(css, "font-feature-settings");
+ }
+ }
+
+ Glib::ustring
+ FontVariants::get_markup() {
+
+ Glib::ustring markup;
+
+ // Ligatures
+ bool common = _ligatures_common.get_active();
+ bool discretionary = _ligatures_discretionary.get_active();
+ bool historical = _ligatures_historical.get_active();
+ bool contextual = _ligatures_contextual.get_active();
+
+ if (!common) markup += "liga=0,clig=0,"; // On by default.
+ if (discretionary) markup += "dlig=1,";
+ if (historical) markup += "hlig=1,";
+ if (contextual) markup += "calt=1,";
+
+ // Position
+ if ( _position_sub.get_active() ) markup += "subs=1,";
+ else if ( _position_super.get_active() ) markup += "sups=1,";
+
+ // Caps
+ if ( _caps_small.get_active() ) markup += "smcp=1,";
+ else if ( _caps_all_small.get_active() ) markup += "c2sc=1,smcp=1,";
+ else if ( _caps_petite.get_active() ) markup += "pcap=1,";
+ else if ( _caps_all_petite.get_active() ) markup += "c2pc=1,pcap=1,";
+ else if ( _caps_unicase.get_active() ) markup += "unic=1,";
+ else if ( _caps_titling.get_active() ) markup += "titl=1,";
+
+ // Numeric
+ bool default_style = _numeric_default_style.get_active();
+ bool lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool default_width = _numeric_default_width.get_active();
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.get_active();
+
+ bool default_fractions = _numeric_default_fractions.get_active();
+ bool diagonal = _numeric_diagonal.get_active();
+ bool stacked = _numeric_stacked.get_active();
+
+ bool ordinal = _numeric_ordinal.get_active();
+ bool slashed_zero = _numeric_slashed_zero.get_active();
+
+ if (lining) markup += "lnum=1,";
+ if (old_style) markup += "onum=1,";
+ if (proportional) markup += "pnum=1,";
+ if (tabular) markup += "tnum=1,";
+ if (diagonal) markup += "frac=1,";
+ if (stacked) markup += "afrc=1,";
+ if (ordinal) markup += "ordn=1,";
+ if (slashed_zero) markup += "zero=1,";
+
+ // East Asian
+ bool default_variant = _asian_default_variant.get_active();
+ bool jis78 = _asian_jis78.get_active();
+ bool jis83 = _asian_jis83.get_active();
+ bool jis90 = _asian_jis90.get_active();
+ bool jis04 = _asian_jis04.get_active();
+ bool simplified = _asian_simplified.get_active();
+ bool traditional = _asian_traditional.get_active();
+ bool asian_width = _asian_default_width.get_active();
+ bool fwid = _asian_full_width.get_active();
+ bool pwid = _asian_proportional_width.get_active();
+ bool ruby = _asian_ruby.get_active();
+
+ if (jis78 ) markup += "jp78=1,";
+ if (jis83 ) markup += "jp83=1,";
+ if (jis90 ) markup += "jp90=1,";
+ if (jis04 ) markup += "jp04=1,";
+ if (simplified ) markup += "smpl=1,";
+ if (traditional ) markup += "trad=1,";
+
+ if (fwid ) markup += "fwid=1,";
+ if (pwid ) markup += "pwid=1,";
+
+ if (ruby ) markup += "ruby=1,";
+
+ // Feature settings
+ Glib::ustring feature_string;
+ for (auto i: _features) {
+ feature_string += i.second->get_css();
+ }
+
+ feature_string += _feature_entry.get_text();
+ if (!feature_string.empty()) {
+ markup += feature_string;
+ }
+
+ // std::cout << "|" << markup << "|" << std::endl;
+ return markup;
+ }
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/font-variants.h b/src/ui/widget/font-variants.h
new file mode 100644
index 0000000..83c9fa9
--- /dev/null
+++ b/src/ui/widget/font-variants.h
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2015, 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_VARIANT_H
+#define INKSCAPE_UI_WIDGET_FONT_VARIANT_H
+
+#include <gtkmm/expander.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/grid.h>
+
+class SPDesktop;
+class SPObject;
+class SPStyle;
+class SPCSSAttr;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Feature;
+
+/**
+ * A container for selecting font variants (OpenType Features).
+ */
+class FontVariants : public Gtk::VBox
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontVariants();
+
+protected:
+ // Ligatures: To start, use four check buttons.
+ Gtk::Expander _ligatures_frame;
+ Gtk::Grid _ligatures_grid;
+ Gtk::CheckButton _ligatures_common;
+ Gtk::CheckButton _ligatures_discretionary;
+ Gtk::CheckButton _ligatures_historical;
+ Gtk::CheckButton _ligatures_contextual;
+ Gtk::Label _ligatures_label_common;
+ Gtk::Label _ligatures_label_discretionary;
+ Gtk::Label _ligatures_label_historical;
+ Gtk::Label _ligatures_label_contextual;
+
+ // Position: Exclusive options
+ Gtk::Expander _position_frame;
+ Gtk::Grid _position_grid;
+ Gtk::RadioButton _position_normal;
+ Gtk::RadioButton _position_sub;
+ Gtk::RadioButton _position_super;
+
+ // Caps: Exclusive options (maybe a dropdown menu to save space?)
+ Gtk::Expander _caps_frame;
+ Gtk::Grid _caps_grid;
+ Gtk::RadioButton _caps_normal;
+ Gtk::RadioButton _caps_small;
+ Gtk::RadioButton _caps_all_small;
+ Gtk::RadioButton _caps_petite;
+ Gtk::RadioButton _caps_all_petite;
+ Gtk::RadioButton _caps_unicase;
+ Gtk::RadioButton _caps_titling;
+
+ // Numeric: Complicated!
+ Gtk::Expander _numeric_frame;
+ Gtk::Grid _numeric_grid;
+
+ Gtk::RadioButton _numeric_default_style;
+ Gtk::RadioButton _numeric_lining;
+ Gtk::Label _numeric_lining_label;
+ Gtk::RadioButton _numeric_old_style;
+ Gtk::Label _numeric_old_style_label;
+
+ Gtk::RadioButton _numeric_default_width;
+ Gtk::RadioButton _numeric_proportional;
+ Gtk::Label _numeric_proportional_label;
+ Gtk::RadioButton _numeric_tabular;
+ Gtk::Label _numeric_tabular_label;
+
+ Gtk::RadioButton _numeric_default_fractions;
+ Gtk::RadioButton _numeric_diagonal;
+ Gtk::Label _numeric_diagonal_label;
+ Gtk::RadioButton _numeric_stacked;
+ Gtk::Label _numeric_stacked_label;
+
+ Gtk::CheckButton _numeric_ordinal;
+ Gtk::Label _numeric_ordinal_label;
+
+ Gtk::CheckButton _numeric_slashed_zero;
+ Gtk::Label _numeric_slashed_zero_label;
+
+ // East Asian: Complicated!
+ Gtk::Expander _asian_frame;
+ Gtk::Grid _asian_grid;
+
+ Gtk::RadioButton _asian_default_variant;
+ Gtk::RadioButton _asian_jis78;
+ Gtk::RadioButton _asian_jis83;
+ Gtk::RadioButton _asian_jis90;
+ Gtk::RadioButton _asian_jis04;
+ Gtk::RadioButton _asian_simplified;
+ Gtk::RadioButton _asian_traditional;
+
+ Gtk::RadioButton _asian_default_width;
+ Gtk::RadioButton _asian_full_width;
+ Gtk::RadioButton _asian_proportional_width;
+
+ Gtk::CheckButton _asian_ruby;
+
+ // -----
+ Gtk::Expander _feature_frame;
+ Gtk::Grid _feature_grid;
+ Gtk::VBox _feature_vbox;
+ Gtk::Entry _feature_entry;
+ Gtk::Label _feature_label;
+ Gtk::Label _feature_list;
+ Gtk::Label _feature_substitutions;
+
+private:
+ void ligatures_init();
+ void ligatures_callback();
+
+ void position_init();
+ void position_callback();
+
+ void caps_init();
+ void caps_callback();
+
+ void numeric_init();
+ void numeric_callback();
+
+ void asian_init();
+ void asian_callback();
+
+ void feature_init();
+public:
+ void feature_callback();
+
+private:
+ // To determine if we need to write out property (may not be necessary)
+ unsigned _ligatures_all;
+ unsigned _position_all;
+ unsigned _caps_all;
+ unsigned _numeric_all;
+ unsigned _asian_all;
+
+ unsigned _ligatures_mix;
+ unsigned _position_mix;
+ unsigned _caps_mix;
+ unsigned _numeric_mix;
+ unsigned _asian_mix;
+
+ bool _ligatures_changed;
+ bool _position_changed;
+ bool _caps_changed;
+ bool _numeric_changed;
+ bool _feature_changed;
+ bool _asian_changed;
+
+ std::map<std::string, Feature*> _features;
+
+ sigc::signal<void> _changed_signal;
+
+public:
+
+ /**
+ * Update GUI based on query results.
+ */
+ void update( SPStyle const *query, bool different_features, Glib::ustring& font_spec );
+
+ /**
+ * Update GUI based on OpenType features of selected font.
+ */
+ void update_opentype( Glib::ustring& font_spec );
+
+ /**
+ * Fill SPCSSAttr based on settings of buttons.
+ */
+ void fill_css( SPCSSAttr* css );
+
+ /**
+ * Get CSS string for markup.
+ */
+ Glib::ustring get_markup();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return _changed_signal.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_VARIANT_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 :
diff --git a/src/ui/widget/font-variations.cpp b/src/ui/widget/font-variations.cpp
new file mode 100644
index 0000000..fae5cc3
--- /dev/null
+++ b/src/ui/widget/font-variations.cpp
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Felipe CorrĂȘa da Silva Sanches, Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+#include <iomanip>
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <libnrtype/font-instance.h>
+
+#include "font-variations.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontVariationAxis::FontVariationAxis (Glib::ustring name, OTVarAxis& axis)
+ : name (name)
+{
+
+ // std::cout << "FontVariationAxis::FontVariationAxis:: "
+ // << " name: " << name
+ // << " min: " << axis.minimum
+ // << " max: " << axis.maximum
+ // << " val: " << axis.set_val << std::endl;
+
+ label = Gtk::manage( new Gtk::Label( name ) );
+ add( *label );
+
+ precision = 2 - int( log10(axis.maximum - axis.minimum));
+ if (precision < 0) precision = 0;
+
+ scale = Gtk::manage( new Gtk::Scale() );
+ scale->set_range (axis.minimum, axis.maximum);
+ scale->set_value (axis.set_val);
+ scale->set_digits (precision);
+ scale->set_hexpand(true);
+ add( *scale );
+}
+
+
+// ------------------------------------------------------------- //
+
+FontVariations::FontVariations () :
+ Gtk::Grid ()
+{
+ // std::cout << "FontVariations::FontVariations" << std::endl;
+ set_orientation( Gtk::ORIENTATION_VERTICAL );
+ set_name ("FontVariations");
+ size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ show_all_children();
+}
+
+
+// Update GUI based on query.
+void
+FontVariations::update (const Glib::ustring& font_spec) {
+
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification (font_spec.c_str());
+
+ auto children = get_children();
+ for (auto child: children) {
+ remove ( *child );
+ }
+ axes.clear();
+
+ for (auto a: res->openTypeVarAxes) {
+ // std::cout << "Creating axis: " << a.first << std::endl;
+ FontVariationAxis* axis = Gtk::manage( new FontVariationAxis( a.first, a.second ));
+ axes.push_back( axis );
+ add( *axis );
+ size_group->add_widget( *(axis->get_label()) ); // Keep labels the same width
+ axis->get_scale()->signal_value_changed().connect(
+ sigc::mem_fun(*this, &FontVariations::on_variations_change)
+ );
+ }
+
+ show_all_children();
+}
+
+void
+FontVariations::fill_css( SPCSSAttr *css ) {
+
+ // Eventually will want to favor using 'font-weight', etc. but at the moment these
+ // can't handle "fractional" values. See CSS Fonts Module Level 4.
+ sp_repr_css_set_property(css, "font-variation-settings", get_css_string().c_str());
+}
+
+Glib::ustring
+FontVariations::get_css_string() {
+
+ Glib::ustring css_string;
+
+ for (auto axis: axes) {
+ Glib::ustring name = axis->get_name();
+
+ // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.)
+ if (name == "Width") name = "wdth"; // 'font-stretch'
+ if (name == "Weight") name = "wght"; // 'font-weight'
+ if (name == "Optical size") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution.
+ if (name == "Slant") name = "slnt"; // 'font-style'
+ if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic.
+
+ std::stringstream value;
+ value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value();
+ css_string += "'" + name + "' " + value.str() + "', ";
+ }
+
+ return css_string;
+}
+
+Glib::ustring
+FontVariations::get_pango_string() {
+
+ Glib::ustring pango_string;
+
+ if (!axes.empty()) {
+
+ pango_string += "@";
+
+ for (auto axis: axes) {
+ if (axis->get_value() == 0) continue; // TEMP: Should check against default value.
+ Glib::ustring name = axis->get_name();
+
+ // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.)
+ if (name == "Width") name = "wdth"; // 'font-stretch'
+ if (name == "Weight") name = "wght"; // 'font-weight'
+ if (name == "Optical size") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution.
+ if (name == "Slant") name = "slnt"; // 'font-style'
+ if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic.
+
+ std::stringstream value;
+ value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value();
+ pango_string += name + "=" + value.str() + ",";
+ }
+
+ pango_string.erase (pango_string.size() - 1); // Erase last ','
+ }
+
+ return pango_string;
+}
+
+void
+FontVariations::on_variations_change() {
+ // std::cout << "FontVariations::on_variations_change: " << get_css_string() << std::endl;;
+ signal_changed.emit ();
+}
+
+bool FontVariations::variations_present() const {
+ return !axes.empty();
+}
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/font-variations.h b/src/ui/widget/font-variations.h
new file mode 100644
index 0000000..a3d3896
--- /dev/null
+++ b/src/ui/widget/font-variations.h
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Felipe CorrĂȘa da Silva Sanches, Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H
+#define INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/sizegroup.h>
+#include <gtkmm/label.h>
+#include <gtkmm/scale.h>
+
+#include "libnrtype/OpenTypeUtil.h"
+
+#include "style.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+/**
+ * A widget for a single axis: Label and Slider
+ */
+class FontVariationAxis : public Gtk::Grid
+{
+public:
+ FontVariationAxis(Glib::ustring name, OTVarAxis& axis);
+ Glib::ustring get_name() { return name; }
+ Gtk::Label* get_label() { return label; }
+ double get_value() { return scale->get_value(); }
+ int get_precision() { return precision; }
+ Gtk::Scale* get_scale() { return scale; }
+
+private:
+
+ // Widgets
+ Glib::ustring name;
+ Gtk::Label* label;
+ Gtk::Scale* scale;
+
+ int precision;
+
+ // Signals
+ sigc::signal<void> signal_changed;
+};
+
+/**
+ * A widget for selecting font variations (OpenType Variations).
+ */
+class FontVariations : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontVariations();
+
+protected:
+
+public:
+
+ /**
+ * Update GUI.
+ */
+ void update(const Glib::ustring& font_spec);
+
+ /**
+ * Fill SPCSSAttr based on settings of buttons.
+ */
+ void fill_css( SPCSSAttr* css );
+
+ /**
+ * Get CSS String
+ */
+ Glib::ustring get_css_string();
+
+ Glib::ustring get_pango_string();
+
+ void on_variations_change();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return signal_changed.connect(slot);
+ }
+
+ // return true if there are some variations present
+ bool variations_present() const;
+
+private:
+
+ std::vector<FontVariationAxis*> axes;
+ Glib::RefPtr<Gtk::SizeGroup> size_group;
+
+ sigc::signal<void> signal_changed;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_VARIATIONS_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 :
diff --git a/src/ui/widget/frame.cpp b/src/ui/widget/frame.cpp
new file mode 100644
index 0000000..eac4e22
--- /dev/null
+++ b/src/ui/widget/frame.cpp
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Murray C
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "frame.h"
+
+
+// Inkscape::UI::Widget::Frame
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Frame::Frame(Glib::ustring const &label_text /*= ""*/, gboolean label_bold /*= TRUE*/ )
+ : _label(label_text, Gtk::ALIGN_END, Gtk::ALIGN_CENTER, true)
+{
+ set_shadow_type(Gtk::SHADOW_NONE);
+
+ set_label_widget(_label);
+ set_label(label_text, label_bold);
+}
+
+void
+Frame::add(Widget& widget)
+{
+ Gtk::Frame::add(widget);
+ set_padding(4, 0, 8, 0);
+ show_all_children();
+}
+
+void
+Frame::set_label(const Glib::ustring &label_text, gboolean label_bold /*= TRUE*/)
+{
+ if (label_bold) {
+ _label.set_markup(Glib::ustring("<b>") + label_text + "</b>");
+ } else {
+ _label.set_text(label_text);
+ }
+}
+
+void
+Frame::set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right)
+{
+ auto child = get_child();
+
+ if(child)
+ {
+ child->set_margin_top(padding_top);
+ child->set_margin_bottom(padding_bottom);
+ child->set_margin_start(padding_left);
+ child->set_margin_end(padding_right);
+ }
+}
+
+Gtk::Label const *
+Frame::get_label_widget() const
+{
+ return &_label;
+}
+
+} // namespace Widget
+} // 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/widget/frame.h b/src/ui/widget/frame.h
new file mode 100644
index 0000000..b2934b6
--- /dev/null
+++ b/src/ui/widget/frame.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Murray C
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FRAME_H
+#define INKSCAPE_UI_WIDGET_FRAME_H
+
+#include <gtkmm/frame.h>
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Creates a Gnome HIG style indented frame with bold label
+ * See http://developer.gnome.org/hig-book/stable/controls-frames.html.en
+ */
+class Frame : public Gtk::Frame
+{
+public:
+
+ /**
+ * Construct a Frame Widget.
+ *
+ * @param label The frame text.
+ */
+ Frame(Glib::ustring const &label = "", gboolean label_bold = TRUE);
+
+ /**
+ * Return the label widget
+ */
+ Gtk::Label const *get_label_widget() const;
+
+ /**
+ * Add a widget to this frame
+ */
+ void add(Widget& widget) override;
+
+ /**
+ * Set the frame label text and if bold or not
+ */
+ void set_label(const Glib::ustring &label, gboolean label_bold = TRUE);
+
+ /**
+ * Set the frame padding
+ */
+ void set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right);
+
+protected:
+ Gtk::Label _label;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FRAME_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/widget/highlight-picker.cpp b/src/ui/widget/highlight-picker.cpp
new file mode 100644
index 0000000..f35e405
--- /dev/null
+++ b/src/ui/widget/highlight-picker.cpp
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm.h>
+#include <gtkmm/icontheme.h>
+
+#include "display/cairo-utils.h"
+
+#include "highlight-picker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+HighlightPicker::HighlightPicker() :
+ Glib::ObjectBase(typeid(HighlightPicker)),
+ Gtk::CellRendererPixbuf(),
+ _property_active(*this, "active", 0)
+{
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+}
+
+HighlightPicker::~HighlightPicker()
+= default;
+
+void HighlightPicker::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void HighlightPicker::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void HighlightPicker::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ GdkRectangle carea;
+
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 10, 20);
+ cairo_t *ct = cairo_create(s);
+
+ /* Transparent area */
+ carea.x = 0;
+ carea.y = 0;
+ carea.width = 10;
+ carea.height = 20;
+
+ cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard();
+
+ cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height / 2);
+ cairo_set_source(ct, checkers);
+ cairo_fill_preserve(ct);
+ ink_cairo_set_source_rgba32(ct, _property_active.get_value());
+ cairo_fill(ct);
+
+ cairo_pattern_destroy(checkers);
+
+ cairo_rectangle(ct, carea.x, carea.y + carea.height / 2, carea.width, carea.height / 2);
+ ink_cairo_set_source_rgba32(ct, _property_active.get_value() | 0x000000ff);
+ cairo_fill(ct);
+
+ cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height);
+ ink_cairo_set_source_rgba32(ct, 0x333333ff);
+ cairo_set_line_width(ct, 2);
+ cairo_stroke(ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = gdk_pixbuf_new_from_data( cairo_image_surface_get_data(s),
+ GDK_COLORSPACE_RGB, TRUE, 8,
+ 10, 20, cairo_image_surface_get_stride(s),
+ ink_cairo_pixbuf_cleanup, s);
+ convert_pixbuf_argb32_to_normal(pixbuf);
+
+ property_pixbuf() = Glib::wrap(pixbuf);
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool HighlightPicker::activate_vfunc(GdkEvent* /*event*/,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& /*path*/,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ return false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+//should be okay to put this here
+/**
+ * Converts GdkPixbuf's data to premultiplied ARGB.
+ * This function will convert a GdkPixbuf in place into Cairo's native pixel format.
+ * Note that this is a hack intended to save memory. When the pixbuf is in Cairo's format,
+ * using it with GTK will result in corrupted drawings.
+ */
+void
+convert_pixbuf_normal_to_argb32(GdkPixbuf *pb)
+{
+ convert_pixels_pixbuf_to_argb32(
+ gdk_pixbuf_get_pixels(pb),
+ gdk_pixbuf_get_width(pb),
+ gdk_pixbuf_get_height(pb),
+ gdk_pixbuf_get_rowstride(pb));
+}
+
+/**
+ * Converts GdkPixbuf's data back to its native format.
+ * Once this is done, the pixbuf can be used with GTK again.
+ */
+void
+convert_pixbuf_argb32_to_normal(GdkPixbuf *pb)
+{
+ convert_pixels_argb32_to_pixbuf(
+ gdk_pixbuf_get_pixels(pb),
+ gdk_pixbuf_get_width(pb),
+ gdk_pixbuf_get_height(pb),
+ gdk_pixbuf_get_rowstride(pb));
+}
+
+/*
+ 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/widget/highlight-picker.h b/src/ui/widget/highlight-picker.h
new file mode 100644
index 0000000..0b77a23
--- /dev/null
+++ b/src/ui/widget/highlight-picker.h
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_HIGHLIGHT_PICKER_H__
+#define __UI_DIALOG_HIGHLIGHT_PICKER_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class HighlightPicker : public Gtk::CellRendererPixbuf {
+public:
+ HighlightPicker();
+ ~HighlightPicker() override;
+
+ Glib::PropertyProxy<guint32> property_active() { return _property_active.get_proxy(); }
+
+protected:
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+private:
+
+ Glib::Property<guint32> _property_active;
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/iconrenderer.cpp b/src/ui/widget/iconrenderer.cpp
new file mode 100644
index 0000000..4ca250e
--- /dev/null
+++ b/src/ui/widget/iconrenderer.cpp
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/iconrenderer.h"
+
+#include "layertypeicon.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "widgets/toolbox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+IconRenderer::IconRenderer() :
+ Glib::ObjectBase(typeid(IconRenderer)),
+ Gtk::CellRendererPixbuf(),
+ _property_icon(*this, "icon", 0)
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ set_pixbuf();
+}
+
+/*
+ * Called when an icon is clicked.
+ */
+IconRenderer::type_signal_activated IconRenderer::signal_activated()
+{
+ return m_signal_activated;
+}
+
+void IconRenderer::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void IconRenderer::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void IconRenderer::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ set_pixbuf();
+
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool IconRenderer::activate_vfunc(GdkEvent* /*event*/,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ m_signal_activated.emit(path);
+ return true;
+}
+
+void IconRenderer::add_icon(Glib::ustring name)
+{
+ _icons.push_back(sp_get_icon_pixbuf(name.c_str(), GTK_ICON_SIZE_BUTTON));
+}
+
+void IconRenderer::set_pixbuf()
+{
+ int icon_index = property_icon().get_value();
+ if(icon_index >= 0 && icon_index < _icons.size()) {
+ property_pixbuf() = _icons[icon_index];
+ } else {
+ property_pixbuf() = sp_get_icon_pixbuf("image-missing", GTK_ICON_SIZE_BUTTON);
+ }
+}
+
+
+} // namespace Widget
+} // 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/widget/iconrenderer.h b/src/ui/widget/iconrenderer.h
new file mode 100644
index 0000000..662ce5b
--- /dev/null
+++ b/src/ui/widget/iconrenderer.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_ADDTOICON_H__
+#define __UI_DIALOG_ADDTOICON_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconRenderer : public Gtk::CellRendererPixbuf {
+public:
+ IconRenderer();
+ ~IconRenderer() override = default;;
+
+ Glib::PropertyProxy<int> property_icon() { return _property_icon.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+ void add_icon(Glib::ustring name);
+
+ typedef sigc::signal<void, Glib::ustring> type_signal_activated;
+ type_signal_activated signal_activated();
+protected:
+ type_signal_activated m_signal_activated;
+
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+private:
+
+ Glib::Property<int> _property_icon;
+ std::vector<Glib::RefPtr<Gdk::Pixbuf>> _icons;
+ void set_pixbuf();
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp
new file mode 100644
index 0000000..a1d258e
--- /dev/null
+++ b/src/ui/widget/imagetoggler.cpp
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Jon A. Cruz
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2006-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "ui/widget/imagetoggler.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "widgets/toolbox.h"
+
+#include <iostream>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ImageToggler::ImageToggler( char const* on, char const* off) :
+ Glib::ObjectBase(typeid(ImageToggler)),
+ Gtk::CellRendererPixbuf(),
+ _pixOnName(on),
+ _pixOffName(off),
+ _property_active(*this, "active", false),
+ _property_activatable(*this, "activatable", true),
+ _property_pixbuf_on(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_off(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ _property_pixbuf_on = sp_get_icon_pixbuf(_pixOnName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_off = sp_get_icon_pixbuf(_pixOffName, GTK_ICON_SIZE_MENU);
+
+ property_pixbuf() = _property_pixbuf_off.get_value();
+}
+
+void ImageToggler::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void ImageToggler::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void ImageToggler::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ property_pixbuf() = _property_active.get_value() ? _property_pixbuf_on : _property_pixbuf_off;
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool
+ImageToggler::activate_vfunc(GdkEvent* event,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ _signal_pre_toggle.emit(event);
+ _signal_toggled.emit(path);
+
+ return false;
+}
+
+
+} // namespace Widget
+} // 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/widget/imagetoggler.h b/src/ui/widget/imagetoggler.h
new file mode 100644
index 0000000..a03ee37
--- /dev/null
+++ b/src/ui/widget/imagetoggler.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_IMAGETOGGLER_H__
+#define __UI_DIALOG_IMAGETOGGLER_H__
+/*
+ * Authors:
+ * Jon A. Cruz
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2006-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ImageToggler : public Gtk::CellRendererPixbuf {
+public:
+ ImageToggler( char const *on, char const *off);
+ ~ImageToggler() override = default;;
+
+ sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;}
+ sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; }
+
+ Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ Glib::ustring _pixOnName;
+ Glib::ustring _pixOffName;
+
+ Glib::Property<bool> _property_active;
+ Glib::Property<bool> _property_activatable;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_on;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_off;
+
+ sigc::signal<void, const Glib::ustring&> _signal_toggled;
+ sigc::signal<void, GdkEvent const *> _signal_pre_toggle;
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp
new file mode 100644
index 0000000..7e56ee5
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.cpp
@@ -0,0 +1,726 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Color wheel widget. Outer ring for Hue. Inner triangle for Saturation and Value.
+ *
+ * Copyright (C) 2018 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "ink-color-wheel.h"
+
+// A point with a color value.
+class color_point {
+public:
+ color_point() : x(0), y(0), r(0), g(0), b(0) {};
+ color_point(double x, double y, double r, double g, double b) : x(x), y(y), r(r), g(g), b(b) {};
+ color_point(double x, double y, guint32 color) : x(x), y(y),
+ r(((color & 0xff0000) >> 16)/255.0),
+ g(((color & 0xff00) >> 8)/255.0),
+ b(((color & 0xff) )/255.0) {};
+ guint32 get_color() { return (int(r*255) << 16 | int(g*255) << 8 | int(b*255)); };
+ double x;
+ double y;
+ double r;
+ double g;
+ double b;
+};
+
+inline double lerp(const double& v0, const double& v1, const double& t0, const double&t1, const double& t) {
+ double s = 0;
+ if (t0 != t1) {
+ s = (t - t0)/(t1 - t0);
+ }
+ return (1.0 - s) * v0 + s * v1;
+}
+
+inline color_point lerp(const color_point& v0, const color_point& v1, const double &t0, const double &t1, const double& t) {
+ double x = lerp(v0.x, v1.x, t0, t1, t);
+ double y = lerp(v0.y, v1.y, t0, t1, t);
+ double r = lerp(v0.r, v1.r, t0, t1, t);
+ double g = lerp(v0.g, v1.g, t0, t1, t);
+ double b = lerp(v0.b, v1.b, t0, t1, t);
+ return (color_point(x, y, r, g, b));
+}
+
+inline double clamp(const double& value, const double& min, const double& max) {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+}
+
+// h, s, and v in range 0 to 1. Returns rgb value useful for use in Cairo.
+guint32 hsv_to_rgb(double h, double s, double v) {
+
+ if (h < 0.0 || h > 1.0 ||
+ s < 0.0 || s > 1.0 ||
+ v < 0.0 || v > 1.0) {
+ std::cerr << "ColorWheel: hsv_to_rgb: input out of bounds: (0-1)"
+ << " h: " << h << " s: " << s << " v: " << v << std::endl;
+ return 0x0;
+ }
+
+ double r = v;
+ double g = v;
+ double b = v;
+
+ if (s != 0.0) {
+ double c = s * v;
+ if (h == 1.0) h = 0.0;
+ h *= 6.0;
+
+ double f = h - (int)h;
+ double p = v * (1.0 - s);
+ double q = v * (1.0 - s * f);
+ double t = v * (1.0 - s * (1.0 - f));
+
+ switch ((int)h) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ default: g_assert_not_reached();
+ }
+ }
+ guint32 rgb = (((int)floor (r*255 + 0.5) << 16) |
+ ((int)floor (g*255 + 0.5) << 8) |
+ ((int)floor (b*255 + 0.5) ));
+ return rgb;
+}
+
+double luminance(guint32 color)
+{
+ double r(((color & 0xff0000) >> 16)/255.0);
+ double g(((color & 0xff00) >> 8)/255.0);
+ double b(((color & 0xff) )/255.0);
+ return (r * 0.2125 + g * 0.7154 + b * 0.0721);
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorWheel::ColorWheel()
+ : _hue(0.0)
+ , _saturation(1.0)
+ , _value(1.0)
+ , _ring_width(0.2)
+ , _mode(DRAG_NONE)
+ , _focus_on_ring(true)
+{
+ set_name("ColorWheel");
+ add_events(Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::BUTTON_MOTION_MASK |
+ Gdk::KEY_PRESS_MASK );
+ set_can_focus();
+}
+
+void
+ColorWheel::set_rgb(const double& r, const double&g, const double&b, bool override_hue)
+{
+ double Min = std::min({r, g, b});
+ double Max = std::max({r, g, b});
+ _value = Max;
+ if (Min == Max) {
+ if (override_hue) {
+ _hue = 0.0;
+ }
+ } else {
+ if (Max == r) {
+ _hue = ((g-b)/(Max-Min) )/6.0;
+ } else if (Max == g) {
+ _hue = ((b-r)/(Max-Min) + 2)/6.0;
+ } else {
+ _hue = ((r-g)/(Max-Min) + 4)/6.0;
+ }
+ if (_hue < 0.0) {
+ _hue += 1.0;
+ }
+ }
+ if (Max == 0) {
+ _saturation = 0;
+ } else {
+ _saturation = (Max - Min)/Max;
+ }
+}
+
+void
+ColorWheel::get_rgb(double& r, double& g, double& b)
+{
+ guint32 color = get_rgb();
+ r = ((color & 0xff0000) >> 16)/255.0;
+ g = ((color & 0x00ff00) >> 8)/255.0;
+ b = ((color & 0x0000ff) )/255.0;
+}
+
+guint32
+ColorWheel::get_rgb()
+{
+ return hsv_to_rgb(_hue, _saturation, _value);
+}
+
+/* Pad triangle vertically if necessary */
+void
+draw_vertical_padding(color_point p0, color_point p1, int padding, bool pad_upwards,
+ guint32 *buffer, int height, int stride);
+
+bool
+ColorWheel::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+ Gtk::Allocation allocation = get_allocation();
+ const int width = allocation.get_width();
+ const int height = allocation.get_height();
+
+ const int stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width);
+
+ int cx = width/2;
+ int cy = height/2;
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ // Paint ring
+ guint32* buffer_ring = g_new (guint32, height * stride / 4);
+ double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+ double r2_max = (r_max+1) * (r_max+1); // Must expand a bit to avoid edge effects.
+ double r2_min = (r_min-1) * (r_min-1); // Must shrink a bit to avoid edge effects.
+
+ for (int i = 0; i < height; ++i) {
+ guint32* p = buffer_ring + i * width;
+ double dy = (cy - i);
+ for (int j = 0; j < width; ++j) {
+ double dx = (j - cx);
+ double r2 = dx * dx + dy * dy;
+ if (r2 < r2_min || r2 > r2_max) {
+ *p++ = 0; // Save calculation time.
+ } else {
+ double angle = atan2 (dy, dx);
+ if (angle < 0.0) {
+ angle += 2.0 * M_PI;
+ }
+ double hue = angle/(2.0 * M_PI);
+
+ *p++ = hsv_to_rgb(hue, 1.0, 1.0);
+ }
+ }
+ }
+
+ Cairo::RefPtr<::Cairo::ImageSurface> source_ring =
+ ::Cairo::ImageSurface::create((unsigned char *)buffer_ring,
+ Cairo::FORMAT_RGB24,
+ width, height, stride);
+
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ // Paint line on ring in source (so it gets clipped by stroke).
+ double l = 0.0;
+ guint32 color_on_ring = hsv_to_rgb(_hue, 1.0, 1.0);
+ if (luminance(color_on_ring) < 0.5) l = 1.0;
+
+ Cairo::RefPtr<::Cairo::Context> cr_source_ring = ::Cairo::Context::create(source_ring);
+ cr_source_ring->set_source_rgb(l, l, l);
+
+ cr_source_ring->move_to (cx, cy);
+ cr_source_ring->line_to (cx + cos(_hue * M_PI * 2.0) * r_max+1,
+ cy - sin(_hue * M_PI * 2.0) * r_max+1);
+ cr_source_ring->stroke();
+
+ // Paint with ring surface, clipping to ring.
+ cr->save();
+ cr->set_source(source_ring, 0, 0);
+ cr->set_line_width (r_max - r_min);
+ cr->begin_new_path();
+ cr->arc(cx, cy, (r_max + r_min)/2.0, 0, 2.0 * M_PI);
+ cr->stroke();
+ cr->restore();
+
+ g_free(buffer_ring);
+
+ // Draw focus
+ if (has_focus() && _focus_on_ring) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, 0, 0, width, height);
+ }
+
+ // Paint triangle.
+ /* The triangle is painted by first finding color points on the
+ * edges of the triangle at the same y value via linearly
+ * interpolating between corner values, and then interpolating along
+ * x between the those edge points. The interpolation is in sRGB
+ * space which leads to a complicated mapping between x/y and
+ * saturation/value. This was probably done to remove the need to
+ * convert between HSV and RGB for each pixel.
+ * Black corner: v = 0, s = 1
+ * White corner: v = 1, s = 0
+ * Color corner; v = 1, s = 1
+ */
+ const int padding = 3; // Avoid edge artifacts.
+ double x0, y0, x1, y1, x2, y2;
+ triangle_corners(x0, y0, x1, y1, x2, y2);
+ guint32 color0 = hsv_to_rgb(_hue, 1.0, 1.0);
+ guint32 color1 = hsv_to_rgb(_hue, 1.0, 0.0);
+ guint32 color2 = hsv_to_rgb(_hue, 0.0, 1.0);
+
+ color_point p0 (x0, y0, color0);
+ color_point p1 (x1, y1, color1);
+ color_point p2 (x2, y2, color2);
+
+ // Reorder so we paint from top down.
+ if (p1.y > p2.y) {
+ std::swap(p1, p2);
+ }
+
+ if (p0.y > p2.y) {
+ std::swap(p0, p2);
+ }
+
+ if (p0.y > p1.y) {
+ std::swap(p0, p1);
+ }
+
+ guint32* buffer_triangle = g_new (guint32, height * stride / 4);
+
+ for (int y = 0; y < height; ++y) {
+ guint32 *p = buffer_triangle + y * (stride / 4);
+
+ if (p0.y <= y+padding && y-padding < p2.y) {
+
+ // Get values on side at position y.
+ color_point side0;
+ double y_inter = clamp(y, p0.y, p2.y);
+ if (y < p1.y) {
+ side0 = lerp(p0, p1, p0.y, p1.y, y_inter);
+ } else {
+ side0 = lerp(p1, p2, p1.y, p2.y, y_inter);
+ }
+ color_point side1 = lerp(p0, p2, p0.y, p2.y, y_inter);
+
+ // side0 should be on left
+ if (side0.x > side1.x) {
+ std::swap (side0, side1);
+ }
+
+ int x_start = std::max(0, int(side0.x));
+ int x_end = std::min(int(side1.x), width);
+
+ for (int x = 0; x < width; ++x) {
+ if (x <= x_start) {
+ *p++ = side0.get_color();
+ } else if (x < x_end) {
+ *p++ = lerp(side0, side1, side0.x, side1.x, x).get_color();
+ } else {
+ *p++ = side1.get_color();
+ }
+ }
+ }
+ }
+
+ // add vertical padding to each side separately
+ color_point temp_point = lerp(p0, p1, p0.x, p1.x, (p0.x + p1.x) / 2.0);
+ bool pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p0, p1, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ temp_point = lerp(p0, p2, p0.x, p2.x, (p0.x + p2.x) / 2.0);
+ pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p0, p2, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ temp_point = lerp(p1, p2, p1.x, p2.x, (p1.x + p2.x) / 2.0);
+ pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p1, p2, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ Cairo::RefPtr<::Cairo::ImageSurface> source_triangle =
+ ::Cairo::ImageSurface::create((unsigned char *)buffer_triangle,
+ Cairo::FORMAT_RGB24,
+ width, height, stride);
+
+ // Paint with triangle surface, clipping to triangle.
+ cr->save();
+ cr->set_source(source_triangle, 0, 0);
+ cr->move_to(p0.x, p0.y);
+ cr->line_to(p1.x, p1.y);
+ cr->line_to(p2.x, p2.y);
+ cr->close_path();
+ cr->fill();
+ cr->restore();
+
+ g_free(buffer_triangle);
+
+ // Draw marker
+ double mx = x1 + (x2-x1) * _value + (x0-x2) * _saturation * _value;
+ double my = y1 + (y2-y1) * _value + (y0-y2) * _saturation * _value;
+
+ double a = 0.0;
+ guint32 color_at_marker = get_rgb();
+ if (luminance(color_at_marker) < 0.5) a = 1.0;
+
+ cr->set_source_rgb(a, a, a);
+ cr->begin_new_path();
+ cr->arc(mx, my, 4, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Draw focus
+ if (has_focus() && !_focus_on_ring) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, mx-4, my-4, 8, 8); // This doesn't seem to work.
+ cr->set_line_width(0.5);
+ cr->set_source_rgb(1-a, 1-a, 1-a);
+ cr->begin_new_path();
+ cr->arc(mx, my, 7, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ return true;
+}
+
+void
+draw_vertical_padding(color_point p0, color_point p1, int padding, bool pad_upwards,
+ guint32 *buffer, int height, int stride)
+{
+ // skip if horizontal padding is more accurate
+ double gradient = (p1.y - p0.y) / (p1.x - p0.x);
+ if (std::abs(gradient) > 1.0) {
+ return;
+ }
+
+ double min_y = std::min(p0.y, p1.y);
+ double max_y = std::max(p0.y, p1.y);
+
+ double min_x = std::min(p0.x, p1.x);
+ double max_x = std::max(p0.x, p1.x);
+
+ for (int y = min_y; y <= max_y; ++y) {
+ double start_x = lerp(p0, p1, p0.y, p1.y, clamp(y, min_y, max_y)).x;
+ double end_x = lerp(p0, p1, p0.y, p1.y, clamp(y + 1, min_y, max_y)).x;
+ if (start_x > end_x) {
+ std::swap(start_x, end_x);
+ }
+
+ guint32 *p = buffer + y * stride;
+ p += static_cast<int>(start_x);
+ for (int x = start_x; x <= end_x; ++x) {
+ color_point point = lerp(p0, p1, p0.x, p1.x, clamp(x, min_x, max_x));
+ for (int offset = 0; offset <= padding; ++offset) {
+ if (pad_upwards && (point.y - offset) >= 0) {
+ *(p - (offset * stride)) = point.get_color();
+ } else if (!pad_upwards && (point.y + offset) < height) {
+ *(p + (offset * stride)) = point.get_color();
+ }
+ }
+ ++p;
+ }
+ }
+}
+
+// Find triangle corners given hue and radius.
+void
+ColorWheel::triangle_corners(double &x0, double &y0,
+ double &x1, double &y1,
+ double &x2, double &y2)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int width = allocation.get_width();
+ const int height = allocation.get_height();
+
+ int cx = width/2;
+ int cy = height/2;
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+
+ double angle = _hue * 2.0 * M_PI;
+
+ x0 = cx + cos(angle) * r_min;
+ y0 = cy - sin(angle) * r_min;
+ x1 = cx + cos(angle + 2.0 * M_PI / 3.0) * r_min;
+ y1 = cy - sin(angle + 2.0 * M_PI / 3.0) * r_min;
+ x2 = cx + cos(angle + 4.0 * M_PI / 3.0) * r_min;
+ y2 = cy - sin(angle + 4.0 * M_PI / 3.0) * r_min;
+}
+
+void
+ColorWheel::set_from_xy(const double& x, const double& y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int width = allocation.get_width();
+ const int height = allocation.get_height();
+ double cx = width/2.0;
+ double cy = height/2.0;
+ double r = std::min(cx, cy) * (1 - _ring_width);
+
+ // We calculate RGB value under the cursor by rotating the cursor
+ // and triangle by the hue value and looking at position in the
+ // now right pointing triangle.
+ double angle = _hue * 2 * M_PI;
+ double Sin = sin(angle);
+ double Cos = cos(angle);
+ double xp = ((x-cx) * Cos - (y-cy) * Sin) / r;
+ double yp = ((x-cx) * Sin + (y-cy) * Cos) / r;
+
+ double xt = lerp(0.0, 1.0, -0.5, 1.0, xp);
+ xt = clamp(xt, 0, 1);
+
+ double dy = (1-xt) * cos(M_PI/6.0);
+ double yt = lerp(0.0, 1.0, -dy, dy, yp);
+ yt = clamp(yt, 0, 1);
+
+ color_point c0(0, 0, yt, yt, yt); // Grey point along base.
+ color_point c1(0, 0, hsv_to_rgb(_hue, 1, 1)); // Hue point at apex
+ color_point c = lerp(c0, c1, 0, 1, xt);
+
+ set_rgb(c.r, c.g, c.b, false); // Don't override previous hue.
+}
+
+bool
+ColorWheel::is_in_ring(const double& x, const double& y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int width = allocation.get_width();
+ const int height = allocation.get_height();
+
+ int cx = width/2;
+ int cy = height/2;
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+ double r2_max = r_max * r_max;
+ double r2_min = r_min * r_min;
+
+ double dx = x - cx;
+ double dy = y - cy;
+ double r2 = dx * dx + dy * dy;
+
+ return (r2_min < r2 && r2 < r2_max);
+}
+
+bool
+ColorWheel::is_in_triangle(const double& x, const double& y)
+{
+ double x0, y0, x1, y1, x2, y2;
+ triangle_corners(x0, y0, x1, y1, x2, y2);
+
+ double det = (x2-x1) * (y0-y1) - (y2-y1) * (x0-x1);
+ double s = ((x -x1) * (y0-y1) - (y -y1) * (x0-x1)) / det;
+ double t = ((x2-x1) * (y -y1) - (y2-y1) * (x -x1)) / det;
+
+ return (s >= 0.0 && t >= 0.0 && s+t <= 1.0);
+}
+
+bool
+ColorWheel::on_focus(Gtk::DirectionType direction)
+{
+ // In forward direction, focus passes from no focus to ring focus to triangle focus to no focus.
+ if (!has_focus()) {
+ _focus_on_ring = (direction == Gtk::DIR_TAB_FORWARD);
+ grab_focus();
+ return true;
+ }
+
+ // Already have focus
+ bool keep_focus = false;
+
+ switch (direction) {
+ case Gtk::DIR_UP:
+ case Gtk::DIR_LEFT:
+ case Gtk::DIR_TAB_BACKWARD:
+ if (!_focus_on_ring) {
+ _focus_on_ring = true;
+ keep_focus = true;
+ }
+ break;
+
+ case Gtk::DIR_DOWN:
+ case Gtk::DIR_RIGHT:
+ case Gtk::DIR_TAB_FORWARD:
+ if (_focus_on_ring) {
+ _focus_on_ring = false;
+ keep_focus = true;
+ }
+ break;
+ }
+
+ queue_draw(); // Update focus indicators.
+
+ return keep_focus;
+}
+
+bool
+ColorWheel::on_button_press_event(GdkEventButton* event)
+{
+ // Seat is automatically grabbed.
+ double x = event->x;
+ double y = event->y;
+
+ if (is_in_ring(x, y) ) {
+ _mode = DRAG_H;
+ grab_focus();
+ _focus_on_ring = true;
+ return true;
+ }
+
+ if (is_in_triangle(x, y)) {
+ _mode = DRAG_SV;
+ grab_focus();
+ _focus_on_ring = false;
+ return true;
+ }
+
+ return false;
+}
+
+bool
+ColorWheel::on_button_release_event(GdkEventButton* event)
+{
+ _mode = DRAG_NONE;
+ return true;
+}
+
+bool
+ColorWheel::on_motion_notify_event(GdkEventMotion* event)
+{
+ double x = event->x;
+ double y = event->y;
+
+ Gtk::Allocation allocation = get_allocation();
+ const int width = allocation.get_width();
+ const int height = allocation.get_height();
+ double cx = width/2.0;
+ double cy = height/2.0;
+ double r = std::min(cx, cy) * (1 - _ring_width);
+
+ if (_mode == DRAG_H) {
+
+ double angle = -atan2(y-cy, x-cx);
+ if (angle < 0) angle += 2.0 * M_PI;
+ _hue = angle / (2.0 * M_PI);
+
+ queue_draw();
+ _signal_color_changed.emit();
+ return true;
+ }
+
+ if (_mode == DRAG_SV) {
+
+ set_from_xy(x, y);
+ _signal_color_changed.emit();
+ queue_draw();
+ return true;
+ }
+
+ return false;
+}
+
+bool
+ColorWheel::on_key_press_event(GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ double x0, y0, x1, y1, x2, y2;
+ triangle_corners(x0, y0, x1, y1, x2, y2);
+
+ // Marker position
+ double mx = x1 + (x2-x1) * _value + (x0-x2) * _saturation * _value;
+ double my = y1 + (y2-y1) * _value + (y0-y2) * _saturation * _value;
+
+
+ const double delta_hue = 2.0/360.0;
+
+ switch (key) {
+
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (_focus_on_ring) {
+ _hue += delta_hue;
+ } else {
+ my -= 1.0;
+ set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (_focus_on_ring) {
+ _hue -= delta_hue;
+ } else {
+ my += 1.0;
+ set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (_focus_on_ring) {
+ _hue += delta_hue;
+ } else {
+ mx -= 1.0;
+ set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (_focus_on_ring) {
+ _hue -= delta_hue;
+ } else {
+ mx += 1.0;
+ set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+
+ }
+
+ if (consumed) {
+ if (_hue >= 1.0) _hue -= 1.0;
+ if (_hue < 0.0) _hue += 1.0;
+ _signal_color_changed.emit();
+ queue_draw();
+ }
+
+ return consumed;
+}
+
+sigc::signal<void>
+ColorWheel::signal_color_changed()
+{
+ return _signal_color_changed;
+}
+
+} // 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/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h
new file mode 100644
index 0000000..c6333b7
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.h
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Color wheel widget. Outer ring for Hue. Inner triangle for Saturation and Value.
+ *
+ * Copyright (C) 2018 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_COLORWHEEL_H
+#define INK_COLORWHEEL_H
+
+/* Rewrite of the C Gimp ColorWheel which came originally from GTK2. */
+
+#include <gtkmm.h>
+#include <iostream>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorWheel : public Gtk::DrawingArea
+{
+public:
+ ColorWheel();
+ void set_rgb(const double& r, const double& g, const double& b, bool override_hue = true);
+ void get_rgb(double& r, double& g, double& b);
+ guint32 get_rgb();
+ bool is_adjusting() {return _mode != DRAG_NONE;}
+
+protected:
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+
+private:
+ void triangle_corners(double& x0, double& y0,
+ double& x1, double& y1,
+ double& x2, double& y2);
+ void set_from_xy(const double& x, const double& y);
+ bool is_in_ring( const double& x, const double& y);
+ bool is_in_triangle(const double& x, const double& y);
+
+ enum DragMode {
+ DRAG_NONE,
+ DRAG_H,
+ DRAG_SV
+ };
+
+ double _hue; // Range [0,1)
+ double _saturation;
+ double _value;
+ double _ring_width;
+ DragMode _mode;
+ bool _focus_on_ring;
+
+ // Callbacks
+ bool on_focus(Gtk::DirectionType direction) override;
+ bool on_button_press_event(GdkEventButton* event) override;
+ bool on_button_release_event(GdkEventButton* event) override;
+ bool on_motion_notify_event(GdkEventMotion* event) override;
+ bool on_key_press_event(GdkEventKey* key_event) override;
+
+ // Signals
+public:
+ sigc::signal<void> signal_color_changed();
+
+protected:
+ sigc::signal<void> _signal_color_changed;
+
+};
+
+} // Namespace Inkscape
+}
+}
+#endif // INK_COLOR_WHEEL_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/widget/ink-flow-box.cpp b/src/ui/widget/ink-flow-box.cpp
new file mode 100644
index 0000000..8485dd9
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.cpp
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "preferences.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/ink-flow-box.h"
+#include <gtkmm/adjustment.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+InkFlowBox::InkFlowBox(const gchar *name)
+{
+ set_name(name);
+ this->pack_start(_controller, false, false, 0);
+ this->pack_start(_flowbox, true, true, 0);
+ _flowbox.set_activate_on_single_click(true);
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton("", false);
+ tbutton->set_always_show_image(true);
+ _flowbox.set_selection_mode(Gtk::SelectionMode::SELECTION_NONE);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), false);
+ tbutton->set_active(prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true));
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ tbutton->signal_toggled().connect(
+ sigc::bind<Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_global_toggle), tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->hide();
+ tbutton->set_no_show_all(true);
+ showing = 0;
+ sensitive = true;
+}
+
+InkFlowBox::~InkFlowBox() = default;
+
+Glib::ustring InkFlowBox::getPrefsPath(gint pos)
+{
+ return Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/index_") + std::to_string(pos);
+}
+
+bool InkFlowBox::on_filter(Gtk::FlowBoxChild *child)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool(getPrefsPath(child->get_index()), true)) {
+ showing++;
+ return true;
+ }
+ return false;
+}
+
+void InkFlowBox::on_toggle(gint pos, Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool global = prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true);
+ if (global && sensitive) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ dynamic_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ prefs->setBool(getPrefsPath(pos), true);
+ tbutton->set_active(true);
+ sensitive = true;
+ } else {
+ prefs->setBool(getPrefsPath(pos), tbutton->get_active());
+ }
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+void InkFlowBox::on_global_toggle(Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), tbutton->get_active());
+ sensitive = true;
+ if (tbutton->get_active()) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ dynamic_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ }
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ sensitive = true;
+}
+
+void InkFlowBox::insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton(label, true);
+ tbutton->set_active(prefs->getBool(getPrefsPath(pos), active));
+ tbutton->signal_toggled().connect(
+ sigc::bind<gint, Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_toggle), pos, tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->show();
+ prefs->setBool(getPrefsPath(pos), prefs->getBool(getPrefsPath(pos), active));
+ widget->set_size_request(minwidth, -1);
+ _flowbox.insert(*widget, pos);
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+} // namespace Widget
+} // 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/widget/ink-flow-box.h b/src/ui/widget/ink-flow-box.h
new file mode 100644
index 0000000..be84ee9
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_INK_FLOW_BOX_H
+#define INKSCAPE_INK_FLOW_BOX_H
+
+#include <gtkmm/actionbar.h>
+#include <gtkmm/box.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/flowboxchild.h>
+#include <gtkmm/togglebutton.h>
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A flowbox widget with filter controller for dialogs.
+ */
+
+class InkFlowBox : public Gtk::VBox {
+ public:
+ InkFlowBox(const gchar *name);
+ ~InkFlowBox() override;
+ void insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth);
+ void on_toggle(gint pos, Gtk::ToggleButton *tbutton);
+ void on_global_toggle(Gtk::ToggleButton *tbutton);
+ void set_visible(gint pos, bool visible);
+ bool on_filter(Gtk::FlowBoxChild *child);
+ Glib::ustring getPrefsPath(gint pos);
+ /**
+ * Construct a InkFlowBox.
+ */
+
+ private:
+ Gtk::FlowBox _flowbox;
+ Gtk::ActionBar _controller;
+ gint showing;
+ bool sensitive;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_INK_FLOW_BOX_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/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..3bb117a
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,473 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * Rewrite of the 'C' ruler code which came originally from Gimp.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "ink-ruler.h"
+
+#include <iostream>
+#include <cmath>
+
+#include "util/units.h"
+
+struct SPRulerMetric
+{
+ gdouble ruler_scale[16];
+ gint subdivide[5];
+};
+
+// Ruler metric for general use.
+static SPRulerMetric const ruler_metric_general = {
+ { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 },
+ { 1, 5, 10, 50, 100 }
+};
+
+// Ruler metric for inch scales.
+static SPRulerMetric const ruler_metric_inches = {
+ { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 },
+ { 1, 2, 4, 8, 16 }
+};
+
+// Half width of pointer triangle.
+static double half_width = 5.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Ruler::Ruler(Gtk::Orientation orientation)
+ : _orientation(orientation)
+ , _backing_store(nullptr)
+ , _lower(0)
+ , _upper(1000)
+ , _max_size(1000)
+ , _unit(nullptr)
+ , _backing_store_valid(false)
+ , _rect()
+ , _position(0)
+{
+ set_name("InkRuler");
+
+ set_events(Gdk::POINTER_MOTION_MASK);
+
+ signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback));
+}
+
+// Set display unit for ruler.
+void
+Ruler::set_unit(Inkscape::Util::Unit const *unit)
+{
+ if (_unit != unit) {
+ _unit = unit;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Set range for ruler, update ticks.
+void
+Ruler::set_range(const double& lower, const double& upper)
+{
+ if (_lower != lower || _upper != upper) {
+
+ _lower = lower;
+ _upper = upper;
+ _max_size = _upper - _lower;
+ if (_max_size == 0) {
+ _max_size = 1;
+ }
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as
+// our ruler is tied tightly to the canvas, if one is destroyed, so is the other.
+void
+Ruler::add_track_widget(Gtk::Widget& widget)
+{
+ widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback), false); // false => connect first
+}
+
+
+// Draws marker in response to motion events from canvas. Position is defined in ruler pixel
+// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If
+// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates.
+bool
+Ruler::draw_marker_callback(GdkEventMotion* motion_event)
+{
+ double position = 0;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ position = motion_event->x;
+ } else {
+ position = motion_event->y;
+ }
+
+ if (position != _position) {
+
+ _position = position;
+
+ // Find region to repaint (old and new marker positions).
+ Cairo::RectangleInt new_rect = marker_rect();
+ Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect);
+ region->do_union(_rect);
+
+ // Queue repaint
+ queue_draw_region(region);
+
+ _rect = new_rect;
+ }
+
+ return false;
+}
+
+
+// Find smallest dimension of ruler based on font size.
+void
+Ruler::size_request (Gtk::Requisition& requisition) const
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ // Get font size
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ int size = 2 + font_size * 2.0; // Room for labels and ticks
+
+ int width = border.get_left() + border.get_right();
+ int height = border.get_top() + border.get_bottom();
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ width += 1;
+ height += size;
+ } else {
+ width += size;
+ height += 1;
+ }
+
+ // Only valid for orientation in question (smallest dimension)!
+ requisition.width = width;
+ requisition.height = height;
+}
+
+void
+Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_width = natural_width = requisition.width;
+}
+
+void
+Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_height = natural_height = requisition.height;
+}
+
+// Update backing store when scale changes.
+// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there
+// is a border, we calculate tick position ignoring border width at ends of ruler but move the
+// ticks and position marker inside the border.
+bool
+Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // if (allocation.get_x() != 0 || allocation.get_y() != 0) {
+ // std::cerr << "Ruler::draw_scale: maybe we do have to handle allocation x and y! "
+ // << " x: " << allocation.get_x() << " y: " << allocation.get_y() << std::endl;
+ // }
+
+ // Create backing store (need surface_in to get scale factor correct).
+ Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target();
+ _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight);
+
+ // Get context
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store);
+ style_context->render_background(cr, 0, 0, awidth, aheight);
+
+ cr->set_line_width(1.0);
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+
+ // Ruler size (only smallest dimension used later).
+ int rwidth = awidth - (border.get_left() + border.get_right());
+ int rheight = aheight - (border.get_top() + border.get_bottom());
+
+ // Draw bottom/right line of ruler
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle( 0, aheight - border.get_bottom() - 1, awidth, 1);
+ } else {
+ cr->rectangle( awidth - border.get_left() - 1, 0, 1, aheight);
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ cr->fill();
+
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+
+ // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler.
+ // We actually require twice the distance.
+ unsigned int scale = std::ceil (_max_size); // Largest number
+ Glib::ustring scale_text = std::to_string(scale);
+ unsigned int digits = scale_text.length() + 1; // Add one for negative sign.
+ unsigned int minimum = digits * font_size * 2;
+
+ double pixels_per_unit = awidth/_max_size; // pixel per distance
+
+ SPRulerMetric ruler_metric = ruler_metric_general;
+ if (_unit == Inkscape::Util::unit_table.getUnit("in")) {
+ ruler_metric = ruler_metric_inches;
+ }
+
+ unsigned scale_index;
+ for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break;
+ }
+
+ // Now we find out what is the subdivide index for the closest ticks we can draw
+ unsigned divide_index;
+ for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break;
+ }
+
+ // We'll loop over all ticks.
+ double pixels_per_tick = pixels_per_unit *
+ ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index];
+
+ double units_per_tick = pixels_per_tick/pixels_per_unit;
+ double ticks_per_unit = 1.0/units_per_tick;
+
+ // Find first and last ticks
+ int start = 0;
+ int end = 0;
+ if (_lower < _upper) {
+ start = std::floor (_lower * ticks_per_unit);
+ end = std::ceil (_upper * ticks_per_unit);
+ } else {
+ start = std::floor (_upper * ticks_per_unit);
+ end = std::ceil (_lower * ticks_per_unit);
+ }
+
+ // std::cout << " start: " << start
+ // << " end: " << end
+ // << " pixels_per_unit: " << pixels_per_unit
+ // << " pixels_per_tick: " << pixels_per_tick
+ // << std::endl;
+
+ // Loop over all ticks
+ for (int i = start; i < end+1; ++i) {
+
+ // Position of tick (add 0.5 to center tick on pixel).
+ double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5;
+
+ // Height of tick
+ int height = rheight;
+ for (int j = divide_index; j > 0; --j) {
+ if (i%ruler_metric.subdivide[j] == 0) break;
+ height = height/2 + 1;
+ }
+
+ // Draw text for major ticks.
+ if (i%ruler_metric.subdivide[divide_index] == 0) {
+
+ int label_value = std::round(i*units_per_tick);
+ Glib::ustring label = std::to_string(label_value);
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout("");
+ layout->set_font_description(font);
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ layout->set_text(label);
+ cr->move_to (position+2, border.get_top());
+ layout->show_in_cairo_context(cr);
+ } else {
+ cr->move_to (border.get_left(), position);
+ int n = 0;
+ for (char const &c : label) {
+ std::string s(1, c);
+ layout->set_text(s);
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ cr->move_to(border.get_left() + (aheight-text_width)/2.0 - 1,
+ position + n*0.7*text_height - 1);
+ layout->show_in_cairo_context(cr);
+ ++n;
+ }
+ // Glyphs are not centered in vertical text... should specify fixed width numbers.
+ // Glib::RefPtr<Pango::Context> context = layout->get_context();
+ // if (_orientation == Gtk::ORIENTATION_VERTICAL) {
+ // context->set_base_gravity(Pango::GRAVITY_EAST);
+ // context->set_gravity_hint(Pango::GRAVITY_HINT_STRONG);
+ // cr->move_to(...)
+ // cr->save();
+ // cr->rotate(M_PI_2);
+ // layout->show_in_cairo_context(cr);
+ // cr->restore();
+ // }
+ }
+ }
+
+ // Draw ticks
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(position, rheight + border.get_top() - height);
+ cr->line_to(position, rheight + border.get_top());
+ } else {
+ cr->move_to(rheight + border.get_left() - height, position);
+ cr->line_to(rheight + border.get_left(), position);
+ }
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+
+ return true;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ // Temp (to verify our redraw rectangle encloses position marker).
+ // Cairo::RectangleInt rect = marker_rect();
+ // cr->set_source_rgb(0, 1.0, 0);
+ // cr->rectangle (rect.x, rect.y, rect.width, rect.height);
+ // cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ double offset = aheight - border.get_bottom();
+ cr->move_to(_position, offset);
+ cr->line_to(_position - half_width, offset - half_width);
+ cr->line_to(_position + half_width, offset - half_width);
+ cr->close_path();
+ } else {
+ double offset = awidth - border.get_right();
+ cr->move_to(offset, _position);
+ cr->line_to(offset - half_width, _position - half_width);
+ cr->line_to(offset - half_width, _position + half_width);
+ cr->close_path();
+ }
+ cr->fill();
+}
+
+// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the
+// redraw area.
+Cairo::RectangleInt
+Ruler::marker_rect()
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ int rwidth = awidth - border.get_left() - border.get_right();
+ int rheight = aheight - border.get_top() - border.get_bottom();
+
+ Cairo::RectangleInt rect;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+
+ // Find size of rectangle to enclose triangle.
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ rect.x = std::floor(_position - half_width);
+ rect.y = std::floor(border.get_top() + rheight - half_width);
+ rect.width = std::ceil(half_width * 2.0 + 1);
+ rect.height = std::ceil(half_width);
+ } else {
+ rect.x = std::floor(border.get_left() + rwidth - half_width);
+ rect.y = std::floor(_position - half_width);
+ rect.width = std::ceil(half_width);
+ rect.height = std::ceil(half_width * 2.0 + 1);
+ }
+
+ return rect;
+}
+
+// Draw the ruler using the tick backing store.
+bool
+Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ if (!_backing_store_valid) {
+ draw_scale (cr);
+ }
+
+ cr->set_source (_backing_store, 0, 0);
+ cr->paint();
+
+ draw_marker (cr);
+
+ return true;
+}
+
+// Update ruler on style change (font-size, etc.)
+void
+Ruler::on_style_updated() {
+
+ Gtk::DrawingArea::on_style_updated();
+
+ _backing_store_valid = false; // If font-size changed we need to regenerate store.
+
+ queue_resize();
+ queue_draw();
+}
+
+} // 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/widget/ink-ruler.h b/src/ui/widget/ink-ruler.h
new file mode 100644
index 0000000..0b9f9af
--- /dev/null
+++ b/src/ui/widget/ink-ruler.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_RULER_H
+#define INK_RULER_H
+
+/* Rewrite of the C Ruler. */
+
+#include <gtkmm.h>
+
+namespace Inkscape {
+namespace Util {
+class Unit;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Ruler : public Gtk::DrawingArea
+{
+public:
+ Ruler(Gtk::Orientation orientation);
+
+ void set_unit(Inkscape::Util::Unit const *unit);
+ void set_range(const double& lower, const double& upper);
+
+ void add_track_widget(Gtk::Widget& widget);
+ bool draw_marker_callback(GdkEventMotion* motion_event);
+
+ void size_request(Gtk::Requisition& requisition) const;
+ void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+
+protected:
+ bool draw_scale(const Cairo::RefPtr<::Cairo::Context>& cr);
+ void draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr);
+ Cairo::RectangleInt marker_rect();
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+ void on_style_updated() override;
+
+private:
+ Gtk::Orientation _orientation;
+
+ Inkscape::Util::Unit const* _unit;
+ double _lower;
+ double _upper;
+ double _position;
+ double _max_size;
+ double _font_scale;
+
+ bool _backing_store_valid;
+
+ Cairo::RefPtr<::Cairo::Surface> _backing_store;
+ Cairo::RectangleInt _rect;
+};
+
+} // Namespace Inkscape
+}
+}
+#endif // INK_RULER_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/widget/ink-spinscale.cpp b/src/ui/widget/ink-spinscale.cpp
new file mode 100644
index 0000000..7c546c2
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.cpp
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/** \file
+ A widget that allows entering a numerical value either by
+ clicking/dragging on a custom Gtk::Scale or by using a
+ Gtk::SpinButton. The custom Gtk::Scale differs from the stock
+ Gtk::Scale in that it includes a label to save space and has a
+ "slow dragging" mode triggered by the Alt key.
+*/
+
+#include "ink-spinscale.h"
+#include <gdkmm/general.h>
+#include <gdkmm/cursor.h>
+#include <gdkmm/event.h>
+
+#include <gtkmm/spinbutton.h>
+
+#include <gdk/gdk.h>
+
+#include <iostream>
+#include <utility>
+
+InkScale::InkScale(Glib::RefPtr<Gtk::Adjustment> adjustment, Gtk::SpinButton* spinbutton)
+ : Glib::ObjectBase("InkScale")
+ , Gtk::Scale(adjustment)
+ , _spinbutton(spinbutton)
+ , _dragging(false)
+ , _drag_start(0)
+ , _drag_offset(0)
+{
+ set_name("InkScale");
+ // std::cout << "GType name: " << G_OBJECT_TYPE_NAME(gobj()) << std::endl;
+}
+
+void
+InkScale::set_label(Glib::ustring label) {
+ _label = label;
+}
+
+bool
+InkScale::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ Gtk::Range::on_draw(cr);
+
+ // Get SpinButton style info...
+ auto style_spin = _spinbutton->get_style_context();
+ auto state_spin = style_spin->get_state();
+ Gdk::RGBA text_color = style_spin->get_color( state_spin );
+
+ // Create Pango layout.
+ auto layout_label = create_pango_layout(_label);
+ layout_label->set_ellipsize( Pango::ELLIPSIZE_END );
+ layout_label->set_width(PANGO_SCALE * get_width());
+
+ // Get y location of SpinButton text (to match vertical position of SpinButton text).
+ int x, y;
+ _spinbutton->get_layout_offsets(x, y);
+
+ // Fill widget proportional to value.
+ double fraction = get_fraction();
+
+ // Get trough rectangle and clipping point for text.
+ Gdk::Rectangle slider_area = get_range_rect();
+ double clip_text_x = slider_area.get_x() + slider_area.get_width() * fraction;
+
+ // Render text in normal text color.
+ cr->save();
+ cr->rectangle(clip_text_x, 0, get_width(), get_height());
+ cr->clip();
+ Gdk::Cairo::set_source_rgba(cr, text_color);
+ //cr->set_source_rgba(0, 0, 0, 1);
+ cr->move_to(5, y );
+ layout_label->show_in_cairo_context(cr);
+ cr->restore();
+
+ // Render text, clipped, in white over bar (TODO: use same color as SpinButton progress bar).
+ cr->save();
+ cr->rectangle(0, 0, clip_text_x, get_height());
+ cr->clip();
+ cr->set_source_rgba(1, 1, 1, 1);
+ cr->move_to(5, y);
+ layout_label->show_in_cairo_context(cr);
+ cr->restore();
+
+ return true;
+}
+
+bool
+InkScale::on_button_press_event(GdkEventButton* button_event) {
+
+ if (! (button_event->state & GDK_MOD1_MASK) ) {
+ bool constrained = button_event->state & GDK_CONTROL_MASK;
+ set_adjustment_value(button_event->x, constrained);
+ }
+
+ // Dragging must be initialized after any adjustment due to button press.
+ _dragging = true;
+ _drag_start = button_event->x;
+ _drag_offset = get_width() * get_fraction();
+
+ return true;
+}
+
+bool
+InkScale::on_button_release_event(GdkEventButton* button_event) {
+
+ _dragging = false;
+ return true;
+}
+
+bool
+InkScale::on_motion_notify_event(GdkEventMotion* motion_event) {
+
+ double x = motion_event->x;
+ double y = motion_event->y;
+
+ if (_dragging) {
+
+ if (! (motion_event->state & GDK_MOD1_MASK) ) {
+ // Absolute change
+ bool constrained = motion_event->state & GDK_CONTROL_MASK;
+ set_adjustment_value(x, constrained);
+ } else {
+ // Relative change
+ double xx = (_drag_offset + (x - _drag_start) * 0.1);
+ set_adjustment_value(xx);
+ }
+ return true;
+ }
+
+ if (! (motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) {
+
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, Gdk::SB_UP_ARROW);
+ // Get Gdk::window (not Gtk::window).. set cursor for entire window.
+ // Would need to unset with leave event.
+ // get_window()->set_cursor( cursor );
+
+ // Can't see how to do this the C++ way since GdkEventMotion
+ // is a structure with a C window member. There is a gdkmm
+ // wrapping function for Gdk::EventMotion but only in unstable.
+
+ // If the cursor theme doesn't have the `sb_up_arrow` cursor then the pointer will be NULL
+ if (cursor)
+ gdk_window_set_cursor( motion_event->window, cursor->gobj() );
+ }
+
+ return false;
+}
+
+double
+InkScale::get_fraction() {
+
+ Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment();
+ double upper = adjustment->get_upper();
+ double lower = adjustment->get_lower();
+ double value = adjustment->get_value();
+ double fraction = (value - lower)/(upper - lower);
+
+ return fraction;
+}
+
+void
+InkScale::set_adjustment_value(double x, bool constrained) {
+
+ Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment();
+ double upper = adjustment->get_upper();
+ double lower = adjustment->get_lower();
+ double range = upper-lower;
+
+ Gdk::Rectangle slider_area = get_range_rect();
+ double fraction = (x - slider_area.get_x()) / (double)slider_area.get_width();
+ double value = fraction * range + lower;
+
+ if (constrained) {
+ // TODO: do we want preferences for (any of) these?
+ if (fmod(range+1,16) == 0) {
+ value = round(value/16) * 16;
+ } else if (range >= 1000 && fmod(upper,100) == 0) {
+ value = round(value/100) * 100;
+ } else if (range >= 100 && fmod(upper,10) == 0) {
+ value = round(value/10) * 10;
+ } else if (range > 20 && fmod(upper,5) == 0) {
+ value = round(value/5) * 5;
+ } else if (range > 2) {
+ value = round(value);
+ } else if (range <= 2) {
+ value = round(value*10) / 10;
+ }
+ }
+
+ adjustment->set_value( value );
+}
+
+/*******************************************************************/
+
+InkSpinScale::InkSpinScale(double value, double lower,
+ double upper, double step_increment,
+ double page_increment, double page_size)
+{
+ set_name("InkSpinScale");
+
+ g_assert (upper - lower > 0);
+
+ _adjustment = Gtk::Adjustment::create(value,
+ lower,
+ upper,
+ step_increment,
+ page_increment,
+ page_size);
+
+ _spinbutton = Gtk::manage(new Gtk::SpinButton(_adjustment));
+ _spinbutton->set_numeric();
+ _spinbutton->signal_key_release_event().connect(sigc::mem_fun(*this,&InkSpinScale::on_key_release_event),false);
+
+ _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton));
+ _scale->set_draw_value(false);
+
+ pack_end( *_spinbutton, Gtk::PACK_SHRINK );
+ pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET );
+}
+
+InkSpinScale::InkSpinScale(Glib::RefPtr<Gtk::Adjustment> adjustment)
+ : _adjustment(std::move(adjustment))
+{
+ set_name("InkSpinScale");
+
+ g_assert (_adjustment->get_upper() - _adjustment->get_lower() > 0);
+
+ _spinbutton = Gtk::manage(new Gtk::SpinButton(_adjustment));
+ _spinbutton->set_numeric();
+
+ _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton));
+ _scale->set_draw_value(false);
+
+ pack_end( *_spinbutton, Gtk::PACK_SHRINK );
+ pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET );
+}
+
+void
+InkSpinScale::set_label(Glib::ustring label) {
+ _scale->set_label(label);
+}
+
+void
+InkSpinScale::set_digits(int digits) {
+ _spinbutton->set_digits(digits);
+}
+
+int
+InkSpinScale::get_digits() const {
+ return _spinbutton->get_digits();
+}
+
+void
+InkSpinScale::set_focus_widget(GtkWidget * focus_widget) {
+ _focus_widget = focus_widget;
+}
+
+// Return focus to canvas.
+bool
+InkSpinScale::on_key_release_event(GdkEventKey* key_event) {
+
+ switch (key_event->keyval) {
+ case GDK_KEY_Escape:
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ if (_focus_widget) {
+ gtk_widget_grab_focus( _focus_widget );
+ }
+ }
+ break;
+ }
+
+ return false;
+}
diff --git a/src/ui/widget/ink-spinscale.h b/src/ui/widget/ink-spinscale.h
new file mode 100644
index 0000000..ada7efd
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.h
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INK_SPINSCALE_H
+#define INK_SPINSCALE_H
+
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/**
+ A widget that allows entering a numerical value either by
+ clicking/dragging on a custom Gtk::Scale or by using a
+ Gtk::SpinButton. The custom Gtk::Scale differs from the stock
+ Gtk::Scale in that it includes a label to save space and has a
+ "slow-dragging" mode triggered by the Alt key.
+*/
+
+#include <glibmm/ustring.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/scale.h>
+
+namespace Gtk {
+ class SpinButton;
+}
+
+class InkScale : public Gtk::Scale
+{
+ public:
+ InkScale(Glib::RefPtr<Gtk::Adjustment>, Gtk::SpinButton* spinbutton);
+ ~InkScale() override = default;;
+
+ void set_label(Glib::ustring label);
+
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+
+ protected:
+
+ bool on_button_press_event(GdkEventButton* button_event) override;
+ bool on_button_release_event(GdkEventButton* button_event) override;
+ bool on_motion_notify_event(GdkEventMotion* motion_event) override;
+
+ private:
+
+ double get_fraction();
+ void set_adjustment_value(double x, bool constrained = false);
+
+ Gtk::SpinButton * _spinbutton; // Needed to get placement/text color.
+ Glib::ustring _label;
+
+ bool _dragging;
+ double _drag_start;
+ double _drag_offset;
+};
+
+class InkSpinScale : public Gtk::Box
+{
+ public:
+
+ // Create an InkSpinScale with a new adjustment.
+ InkSpinScale(double value,
+ double lower,
+ double upper,
+ double step_increment = 1,
+ double page_increment = 10,
+ double page_size = 0);
+
+ // Create an InkSpinScale with a preexisting adjustment.
+ InkSpinScale(Glib::RefPtr<Gtk::Adjustment>);
+
+ ~InkSpinScale() override = default;;
+
+ void set_label(Glib::ustring label);
+ void set_digits(int digits);
+ int get_digits() const;
+ void set_focus_widget(GtkWidget *focus_widget);
+ Glib::RefPtr<Gtk::Adjustment> get_adjustment() { return _adjustment; };
+
+ protected:
+
+ InkScale* _scale;
+ Gtk::SpinButton* _spinbutton;
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ GtkWidget* _focus_widget = nullptr;
+
+ bool on_key_release_event(GdkEventKey* key_event) override;
+
+ private:
+
+};
+
+#endif // INK_SPINSCALE_H
diff --git a/src/ui/widget/insertordericon.cpp b/src/ui/widget/insertordericon.cpp
new file mode 100644
index 0000000..0a21f3c
--- /dev/null
+++ b/src/ui/widget/insertordericon.cpp
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/insertordericon.h"
+
+#include "layertypeicon.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "widgets/toolbox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+InsertOrderIcon::InsertOrderIcon() :
+ Glib::ObjectBase(typeid(InsertOrderIcon)),
+ Gtk::CellRendererPixbuf(),
+ _pixTopName(INKSCAPE_ICON("insert-top")),
+ _pixBottomName(INKSCAPE_ICON("insert-bottom")),
+ _property_active(*this, "active", 0),
+ _property_pixbuf_top(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_bottom(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+{
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ _property_pixbuf_top = sp_get_icon_pixbuf(_pixTopName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_bottom = sp_get_icon_pixbuf(_pixBottomName, GTK_ICON_SIZE_MENU);
+
+ property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+}
+
+
+void InsertOrderIcon::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void InsertOrderIcon::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void InsertOrderIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ switch (_property_active.get_value())
+ {
+ case 1:
+ property_pixbuf() = _property_pixbuf_top;
+ break;
+ case 2:
+ property_pixbuf() = _property_pixbuf_bottom;
+ break;
+ default:
+ property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+ break;
+ }
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool InsertOrderIcon::activate_vfunc(GdkEvent* /*event*/,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& /*path*/,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ return false;
+}
+
+
+} // namespace Widget
+} // 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/widget/insertordericon.h b/src/ui/widget/insertordericon.h
new file mode 100644
index 0000000..7ca1cef
--- /dev/null
+++ b/src/ui/widget/insertordericon.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_INSERTORDERICON_H__
+#define __UI_DIALOG_INSERTORDERICON_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class InsertOrderIcon : public Gtk::CellRendererPixbuf {
+public:
+ InsertOrderIcon();
+ ~InsertOrderIcon() override = default;;
+
+ Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ int phys;
+
+ Glib::ustring _pixTopName;
+ Glib::ustring _pixBottomName;
+
+ Glib::Property<int> _property_active;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_top;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_bottom;
+
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/label-tool-item.cpp b/src/ui/widget/label-tool-item.cpp
new file mode 100644
index 0000000..979cfa2
--- /dev/null
+++ b/src/ui/widget/label-tool-item.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A label that can be added to a toolbar
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "label-tool-item.h"
+
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief Create a tool-item containing a label
+ *
+ * \param[in] label The text to display in the label
+ * \param[in] mnemonic True if text should use a mnemonic
+ */
+LabelToolItem::LabelToolItem(const Glib::ustring& label, bool mnemonic)
+ : _label(Gtk::manage(new Gtk::Label(label, mnemonic)))
+{
+ add(*_label);
+ show_all();
+}
+
+/**
+ * \brief Set the markup text in the label
+ *
+ * \param[in] str The markup text
+ */
+void
+LabelToolItem::set_markup(const Glib::ustring& str)
+{
+ _label->set_markup(str);
+}
+
+/**
+ * \brief Sets whether label uses Pango markup
+ *
+ * \param[in] setting true if the label text should be parsed for markup
+ */
+void
+LabelToolItem::set_use_markup(bool setting)
+{
+ _label->set_use_markup(setting);
+}
+
+}
+}
+}
+/*
+ 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/widget/label-tool-item.h b/src/ui/widget/label-tool-item.h
new file mode 100644
index 0000000..1fe6892
--- /dev/null
+++ b/src/ui/widget/label-tool-item.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A label that can be added to a toolbar
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_LABEL_TOOL_ITEM_H
+#define SEEN_LABEL_TOOL_ITEM_H
+
+#include <gtkmm/toolitem.h>
+
+namespace Gtk {
+class Label;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief A label that can be added to a toolbar
+ */
+class LabelToolItem : public Gtk::ToolItem {
+private:
+ Gtk::Label *_label;
+
+public:
+ LabelToolItem(const Glib::ustring& label, bool mnemonic = false);
+
+ void set_markup(const Glib::ustring& str);
+ void set_use_markup(bool setting = true);
+};
+}
+}
+}
+
+#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/widget/labelled.cpp b/src/ui/widget/labelled.cpp
new file mode 100644
index 0000000..b320b70
--- /dev/null
+++ b/src/ui/widget/labelled.cpp
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "labelled.h"
+#include "ui/icon-loader.h"
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Labelled::Labelled(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Gtk::Widget *widget,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : _widget(widget),
+ _label(new Gtk::Label(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, mnemonic)),
+ _suffix(new Gtk::Label(suffix, Gtk::ALIGN_START))
+{
+ g_assert(g_utf8_validate(icon.c_str(), -1, nullptr));
+ if (icon != "") {
+ _icon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
+ pack_start(*_icon, Gtk::PACK_SHRINK);
+ }
+
+ set_spacing(6);
+ // Setting margins separately allows for more control over them
+ set_margin_start(6);
+ set_margin_end(6);
+ pack_start(*Gtk::manage(_label), Gtk::PACK_SHRINK);
+ pack_start(*Gtk::manage(_widget), Gtk::PACK_SHRINK);
+ if (mnemonic) {
+ _label->set_mnemonic_widget(*_widget);
+ }
+ widget->set_tooltip_text(tooltip);
+}
+
+
+void Labelled::setWidgetSizeRequest(int width, int height)
+{
+ if (_widget)
+ _widget->set_size_request(width, height);
+
+
+}
+
+Gtk::Widget const *
+Labelled::getWidget() const
+{
+ return _widget;
+}
+
+Gtk::Label const *
+Labelled::getLabel() const
+{
+ return _label;
+}
+
+void
+Labelled::setLabelText(const Glib::ustring &str)
+{
+ _label->set_text(str);
+}
+
+void
+Labelled::setTooltipText(const Glib::ustring &tooltip)
+{
+ _label->set_tooltip_text(tooltip);
+ _widget->set_tooltip_text(tooltip);
+}
+
+bool Labelled::on_mnemonic_activate ( bool group_cycling )
+{
+ return _widget->mnemonic_activate ( group_cycling );
+}
+
+void
+Labelled::set_hexpand(bool expand)
+{
+ // should only have 2 children, but second child may not be _widget
+ child_property_pack_type(*get_children().back()) = expand ? Gtk::PACK_END
+ : Gtk::PACK_START;
+
+ Gtk::HBox::set_hexpand(expand);
+}
+
+} // namespace Widget
+} // 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/widget/labelled.h b/src/ui/widget/labelled.h
new file mode 100644
index 0000000..4620b1a
--- /dev/null
+++ b/src/ui/widget/labelled.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_LABELLED_H
+#define INKSCAPE_UI_WIDGET_LABELLED_H
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Image;
+class Label;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Adds a label with optional icon or suffix to another widget.
+ */
+class Labelled : public Gtk::HBox
+{
+public:
+
+ /**
+ * Construct a Labelled Widget.
+ *
+ * @param label Label.
+ * @param widget Widget to label; should be allocated with new, as it will
+ * be passed to Gtk::manage().
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the text
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Labelled(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Gtk::Widget *widget,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Allow the setting of the width of the labelled widget
+ */
+ void setWidgetSizeRequest(int width, int height);
+ Gtk::Widget const *getWidget() const;
+ Gtk::Label const *getLabel() const;
+
+ void setLabelText(const Glib::ustring &str);
+ void setTooltipText(const Glib::ustring &tooltip);
+
+ void set_hexpand(bool expand = true);
+
+private:
+ bool on_mnemonic_activate( bool group_cycling ) override;
+
+protected:
+
+ Gtk::Widget *_widget;
+ Gtk::Label *_label;
+ Gtk::Label *_suffix;
+ Gtk::Image *_icon;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_LABELLED_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/widget/layer-selector.cpp b/src/ui/widget/layer-selector.cpp
new file mode 100644
index 0000000..779542c
--- /dev/null
+++ b/src/ui/widget/layer-selector.cpp
@@ -0,0 +1,616 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::Widgets::LayerSelector - layer selector widget
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include "ui/dialog/layer-properties.h"
+#include "ui/icon-loader.h"
+#include <boost/range/adaptor/filtered.hpp>
+#include <boost/range/adaptor/reversed.hpp>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+
+#include "document.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "util/reverse-list.h"
+#include "verbs.h"
+#include "xml/node-event-vector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+class AlternateIcons : public Gtk::HBox {
+public:
+ AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b)
+ : _a(nullptr), _b(nullptr)
+ {
+ set_name("AlternateIcons");
+ if (!a.empty()) {
+ _a = Gtk::manage(sp_get_icon_image(a, size));
+ _a->set_no_show_all(true);
+ add(*_a);
+ }
+ if (!b.empty()) {
+ _b = Gtk::manage(sp_get_icon_image(b, size));
+ _b->set_no_show_all(true);
+ add(*_b);
+ }
+ setState(false);
+ }
+
+ bool state() const { return _state; }
+ void setState(bool state) {
+ _state = state;
+ if (_state) {
+ if (_a) {
+ _a->hide();
+ }
+ if (_b) {
+ _b->show();
+ }
+ } else {
+ if (_a) {
+ _a->show();
+ }
+ if (_b) {
+ _b->hide();
+ }
+ }
+ }
+
+private:
+ Gtk::Image *_a;
+ Gtk::Image *_b;
+ bool _state;
+};
+
+}
+
+/** LayerSelector constructor. Creates lock and hide buttons,
+ * initializes the layer dropdown selector with a label renderer,
+ * and hooks up signal for setting the desktop layer when the
+ * selector is changed.
+ */
+LayerSelector::LayerSelector(SPDesktop *desktop)
+: _desktop(nullptr), _layer(nullptr)
+{
+ set_name("LayerSelector");
+ AlternateIcons *label;
+
+ label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")));
+ _visibility_toggle.add(*label);
+ _visibility_toggle.signal_toggled().connect(
+ sigc::compose(
+ sigc::mem_fun(*label, &AlternateIcons::setState),
+ sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active)
+ )
+ );
+ _visibility_toggled_connection = _visibility_toggle.signal_toggled().connect(
+ sigc::compose(
+ sigc::mem_fun(*this, &LayerSelector::_hideLayer),
+ sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active)
+ )
+ );
+
+ _visibility_toggle.set_relief(Gtk::RELIEF_NONE);
+ _visibility_toggle.set_tooltip_text(_("Toggle current layer visibility"));
+ pack_start(_visibility_toggle, Gtk::PACK_EXPAND_PADDING);
+
+ label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked")));
+ _lock_toggle.add(*label);
+ _lock_toggle.signal_toggled().connect(
+ sigc::compose(
+ sigc::mem_fun(*label, &AlternateIcons::setState),
+ sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active)
+ )
+ );
+ _lock_toggled_connection = _lock_toggle.signal_toggled().connect(
+ sigc::compose(
+ sigc::mem_fun(*this, &LayerSelector::_lockLayer),
+ sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active)
+ )
+ );
+
+ _lock_toggle.set_relief(Gtk::RELIEF_NONE);
+ _lock_toggle.set_tooltip_text(_("Lock or unlock current layer"));
+ pack_start(_lock_toggle, Gtk::PACK_EXPAND_PADDING);
+
+ _selector.set_tooltip_text(_("Current layer"));
+ pack_start(_selector, Gtk::PACK_EXPAND_WIDGET);
+
+ _layer_model = Gtk::ListStore::create(_model_columns);
+ _selector.set_model(_layer_model);
+ _selector.pack_start(_label_renderer);
+ _selector.set_cell_data_func(
+ _label_renderer,
+ sigc::mem_fun(*this, &LayerSelector::_prepareLabelRenderer)
+ );
+
+ _selection_changed_connection = _selector.signal_changed().connect(
+ sigc::mem_fun(*this, &LayerSelector::_setDesktopLayer)
+ );
+ setDesktop(desktop);
+}
+
+/** Destructor - disconnects signal handler
+ */
+LayerSelector::~LayerSelector() {
+ setDesktop(nullptr);
+ _selection_changed_connection.disconnect();
+}
+
+/** Sets the desktop for the widget. First disconnects signals
+ * for the current desktop, then stores the pointer to the
+ * given \a desktop, and attaches its signals to this one.
+ * Then it selects the current layer for the desktop.
+ */
+void LayerSelector::setDesktop(SPDesktop *desktop) {
+ if ( desktop == _desktop ) {
+ return;
+ }
+
+ if (_desktop) {
+// _desktop_shutdown_connection.disconnect();
+ if (_current_layer_changed_connection)
+ _current_layer_changed_connection.disconnect();
+ if (_layers_changed_connection)
+ _layers_changed_connection.disconnect();
+// g_signal_handlers_disconnect_by_func(_desktop, (gpointer)&detach, this);
+ }
+ _desktop = desktop;
+ if (_desktop) {
+ // TODO we need a different signal for this, really..s
+// _desktop_shutdown_connection = _desktop->connectShutdown(
+// sigc::bind (sigc::ptr_fun (detach), this));
+// g_signal_connect_after(_desktop, "shutdown", GCallback(detach), this);
+
+ LayerManager *mgr = _desktop->layer_manager;
+ if ( mgr ) {
+ _current_layer_changed_connection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayerSelector::_selectLayer) );
+ //_layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayerSelector::_updateLayer) );
+ _layers_changed_connection = mgr->connectChanged( sigc::mem_fun(*this, &LayerSelector::_layersChanged) );
+ }
+
+ _selectLayer(_desktop->currentLayer());
+ }
+}
+
+namespace {
+
+class is_layer {
+public:
+ is_layer(SPDesktop *desktop) : _desktop(desktop) {}
+ bool operator()(SPObject &object) const {
+ return _desktop->isLayer(&object);
+ }
+private:
+ SPDesktop *_desktop;
+};
+
+class column_matches_object {
+public:
+ column_matches_object(Gtk::TreeModelColumn<SPObject *> const &column,
+ SPObject &object)
+ : _column(column), _object(object) {}
+ bool operator()(Gtk::TreeModel::const_iterator const &iter) const {
+ SPObject *current=(*iter)[_column];
+ return current == &_object;
+ }
+private:
+ Gtk::TreeModelColumn<SPObject *> const &_column;
+ SPObject &_object;
+};
+
+}
+
+void LayerSelector::_layersChanged()
+{
+ if (_desktop) {
+ /*
+ * This code fixes #166691 but causes issues #1066543 and #1080378.
+ * Comment out until solution found.
+ */
+ //_selectLayer(_desktop->currentLayer());
+ }
+}
+
+/** Selects the given layer in the dropdown selector.
+ */
+void LayerSelector::_selectLayer(SPObject *layer) {
+ using Inkscape::Util::List;
+ using Inkscape::Util::cons;
+ using Inkscape::Util::reverse_list;
+
+ _selection_changed_connection.block();
+ _visibility_toggled_connection.block();
+ _lock_toggled_connection.block();
+
+ while (!_layer_model->children().empty()) {
+ Gtk::ListStore::iterator first_row(_layer_model->children().begin());
+ _destroyEntry(first_row);
+ _layer_model->erase(first_row);
+ }
+
+ SPObject *root=_desktop->currentRoot();
+
+ if (_layer) {
+ sp_object_unref(_layer, nullptr);
+ _layer = nullptr;
+ }
+
+ if (layer) {
+ List<SPObject &> hierarchy=reverse_list<SPObject::ParentIterator>(layer, root);
+ if ( layer == root ) {
+ _buildEntries(0, cons(*root, hierarchy));
+ } else if (hierarchy) {
+ _buildSiblingEntries(0, *root, hierarchy);
+ }
+
+ Gtk::TreeIter row(
+ std::find_if(
+ _layer_model->children().begin(),
+ _layer_model->children().end(),
+ column_matches_object(_model_columns.object, *layer)
+ )
+ );
+ if ( row != _layer_model->children().end() ) {
+ _selector.set_active(row);
+ }
+
+ _layer = layer;
+ sp_object_ref(_layer, nullptr);
+ }
+
+ if ( !layer || layer == root ) {
+ _visibility_toggle.set_sensitive(false);
+ _visibility_toggle.set_active(false);
+ _lock_toggle.set_sensitive(false);
+ _lock_toggle.set_active(false);
+ } else {
+ _visibility_toggle.set_sensitive(true);
+ _visibility_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false ));
+ _lock_toggle.set_sensitive(true);
+ _lock_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false ));
+ }
+
+ _lock_toggled_connection.unblock();
+ _visibility_toggled_connection.unblock();
+ _selection_changed_connection.unblock();
+}
+
+/** Sets the current desktop layer to the actively selected layer.
+ */
+void LayerSelector::_setDesktopLayer() {
+ Gtk::ListStore::iterator selected(_selector.get_active());
+ SPObject *layer=_selector.get_active()->get_value(_model_columns.object);
+ if ( _desktop && layer ) {
+ _current_layer_changed_connection.block();
+ _layers_changed_connection.block();
+
+ _desktop->layer_manager->setCurrentLayer(layer);
+
+ _current_layer_changed_connection.unblock();
+ _layers_changed_connection.unblock();
+
+ _selectLayer(_desktop->currentLayer());
+ }
+ if (_desktop && _desktop->canvas) {
+ gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas));
+ }
+}
+
+/** Creates rows in the _layer_model data structure for each item
+ * in \a hierarchy, to a given \a depth.
+ */
+void LayerSelector::_buildEntries(unsigned depth,
+ Inkscape::Util::List<SPObject &> hierarchy)
+{
+ using Inkscape::Util::List;
+ using Inkscape::Util::rest;
+
+ _buildEntry(depth, *hierarchy);
+
+ List<SPObject &> remainder=rest(hierarchy);
+ if (remainder) {
+ _buildEntries(depth+1, remainder);
+ } else {
+ _buildSiblingEntries(depth+1, *hierarchy, remainder);
+ }
+}
+
+/** Creates entries in the _layer_model data structure for
+ * all siblings of the first child in \a parent.
+ */
+void LayerSelector::_buildSiblingEntries(
+ unsigned depth, SPObject &parent,
+ Inkscape::Util::List<SPObject &> hierarchy
+) {
+ using Inkscape::Util::rest;
+
+ auto siblings = parent.children | boost::adaptors::filtered(is_layer(_desktop)) | boost::adaptors::reversed;
+
+ SPObject *layer( hierarchy ? &*hierarchy : nullptr );
+
+ for (auto& sib: siblings) {
+ _buildEntry(depth, sib);
+ if ( &sib == layer ) {
+ _buildSiblingEntries(depth+1, *layer, rest(hierarchy));
+ }
+ }
+}
+
+namespace {
+
+struct Callbacks {
+ sigc::slot<void> update_row;
+ sigc::slot<void> update_list;
+};
+
+void attribute_changed(Inkscape::XML::Node */*repr*/, gchar const *name,
+ gchar const */*old_value*/, gchar const */*new_value*/,
+ bool /*is_interactive*/, void *data)
+{
+ if ( !std::strcmp(name, "inkscape:groupmode") ) {
+ reinterpret_cast<Callbacks *>(data)->update_list();
+ } else {
+ reinterpret_cast<Callbacks *>(data)->update_row();
+ }
+}
+
+void node_added(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) {
+ gchar const *mode=child->attribute("inkscape:groupmode");
+ if ( mode && !std::strcmp(mode, "layer") ) {
+ reinterpret_cast<Callbacks *>(data)->update_list();
+ }
+}
+
+void node_removed(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) {
+ gchar const *mode=child->attribute("inkscape:groupmode");
+ if ( mode && !std::strcmp(mode, "layer") ) {
+ reinterpret_cast<Callbacks *>(data)->update_list();
+ }
+}
+
+void node_reordered(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child,
+ Inkscape::XML::Node */*old_ref*/, Inkscape::XML::Node */*new_ref*/,
+ void *data)
+{
+ gchar const *mode=child->attribute("inkscape:groupmode");
+ if ( mode && !std::strcmp(mode, "layer") ) {
+ reinterpret_cast<Callbacks *>(data)->update_list();
+ }
+}
+
+void update_row_for_object(SPObject *object,
+ Gtk::TreeModelColumn<SPObject *> const &column,
+ Glib::RefPtr<Gtk::ListStore> const &model)
+{
+ Gtk::TreeIter row(
+ std::find_if(
+ model->children().begin(),
+ model->children().end(),
+ column_matches_object(column, *object)
+ )
+ );
+ if ( row != model->children().end() ) {
+ model->row_changed(model->get_path(row), row);
+ }
+}
+
+void rebuild_all_rows(sigc::slot<void, SPObject *> rebuild, SPDesktop *desktop)
+{
+ rebuild(desktop->currentLayer());
+}
+
+}
+
+void LayerSelector::_protectUpdate(sigc::slot<void> slot) {
+ bool visibility_blocked=_visibility_toggled_connection.blocked();
+ bool lock_blocked=_lock_toggled_connection.blocked();
+ _visibility_toggled_connection.block(true);
+ _lock_toggled_connection.block(true);
+ slot();
+
+ SPObject *layer = _desktop ? _desktop->currentLayer() : nullptr;
+ if ( layer ) {
+ bool wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false );
+ if ( _lock_toggle.get_active() != wantedValue ) {
+ _lock_toggle.set_active( wantedValue );
+ }
+ wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false );
+ if ( _visibility_toggle.get_active() != wantedValue ) {
+ _visibility_toggle.set_active( wantedValue );
+ }
+ }
+ _visibility_toggled_connection.block(visibility_blocked);
+ _lock_toggled_connection.block(lock_blocked);
+}
+
+/** Builds and appends a row in the layer model object.
+ */
+void LayerSelector::_buildEntry(unsigned depth, SPObject &object) {
+ Inkscape::XML::NodeEventVector *vector;
+
+ Callbacks *callbacks=new Callbacks();
+
+ callbacks->update_row = sigc::bind(
+ sigc::mem_fun(*this, &LayerSelector::_protectUpdate),
+ sigc::bind(
+ sigc::ptr_fun(&update_row_for_object),
+ &object, _model_columns.object, _layer_model
+ )
+ );
+
+ SPObject *layer=_desktop->currentLayer();
+ if ( (&object == layer) || (&object == layer->parent) ) {
+ callbacks->update_list = sigc::bind(
+ sigc::mem_fun(*this, &LayerSelector::_protectUpdate),
+ sigc::bind(
+ sigc::ptr_fun(&rebuild_all_rows),
+ sigc::mem_fun(*this, &LayerSelector::_selectLayer),
+ _desktop
+ )
+ );
+
+ Inkscape::XML::NodeEventVector events = {
+ &node_added,
+ &node_removed,
+ &attribute_changed,
+ nullptr,
+ &node_reordered
+ };
+
+ vector = new Inkscape::XML::NodeEventVector(events);
+ } else {
+ Inkscape::XML::NodeEventVector events = {
+ nullptr,
+ nullptr,
+ &attribute_changed,
+ nullptr,
+ nullptr
+ };
+
+ vector = new Inkscape::XML::NodeEventVector(events);
+ }
+
+ Gtk::ListStore::iterator row(_layer_model->append());
+
+ row->set_value(_model_columns.depth, depth);
+
+ sp_object_ref(&object, nullptr);
+ row->set_value(_model_columns.object, &object);
+
+ Inkscape::GC::anchor(object.getRepr());
+ row->set_value(_model_columns.repr, object.getRepr());
+
+ row->set_value(_model_columns.callbacks, reinterpret_cast<void *>(callbacks));
+
+ sp_repr_add_listener(object.getRepr(), vector, callbacks);
+}
+
+/** Removes a row from the _model_columns object, disconnecting listeners
+ * on the slot.
+ */
+void LayerSelector::_destroyEntry(Gtk::ListStore::iterator const &row) {
+ Callbacks *callbacks=reinterpret_cast<Callbacks *>(row->get_value(_model_columns.callbacks));
+ SPObject *object=row->get_value(_model_columns.object);
+ if (object) {
+ sp_object_unref(object, nullptr);
+ }
+ Inkscape::XML::Node *repr=row->get_value(_model_columns.repr);
+ if (repr) {
+ sp_repr_remove_listener_by_data(repr, callbacks);
+ Inkscape::GC::release(repr);
+ }
+ delete callbacks;
+}
+
+/** Formats the label for a given layer row
+ */
+void LayerSelector::_prepareLabelRenderer(
+ Gtk::TreeModel::const_iterator const &row
+) {
+ unsigned depth=(*row)[_model_columns.depth];
+ SPObject *object=(*row)[_model_columns.object];
+ bool label_defaulted(false);
+
+ // TODO: when the currently selected row is removed,
+ // (or before one has been selected) something appears to
+ // "invent" an iterator with null data and try to render it;
+ // where does it come from, and how can we avoid it?
+ if ( object && object->getRepr() ) {
+ SPObject *layer=( _desktop ? _desktop->currentLayer() : nullptr );
+ SPObject *root=( _desktop ? _desktop->currentRoot() : nullptr );
+
+ bool isancestor = !( (layer && (object->parent == layer->parent)) || ((layer == root) && (object->parent == root)));
+
+ bool iscurrent = ( (object == layer) && (object != root) );
+
+ gchar *format = g_strdup_printf (
+ "<span size=\"smaller\" %s><tt>%*s%s</tt>%s%s%s%%s%s%s%s</span>",
+ ( _desktop && _desktop->itemIsHidden (SP_ITEM(object)) ? "foreground=\"gray50\"" : "" ),
+ depth, "", ( iscurrent ? "&#8226;" : " " ),
+ ( iscurrent ? "<b>" : "" ),
+ ( SP_ITEM(object)->isLocked() ? "[" : "" ),
+ ( isancestor ? "<small>" : "" ),
+ ( isancestor ? "</small>" : "" ),
+ ( SP_ITEM(object)->isLocked() ? "]" : "" ),
+ ( iscurrent ? "</b>" : "" )
+ );
+
+ gchar const *label;
+ if ( object != root ) {
+ label = object->label();
+ if (!object->label()) {
+ label = object->defaultLabel();
+ label_defaulted = true;
+ }
+ } else {
+ label = _("(root)");
+ }
+
+ gchar *text = g_markup_printf_escaped(format, ink_ellipsize_text (label, 50).c_str());
+ _label_renderer.property_markup() = text;
+ g_free(text);
+ g_free(format);
+ } else {
+ _label_renderer.property_markup() = "<small> </small>";
+ }
+
+ _label_renderer.property_ypad() = 1;
+ _label_renderer.property_style() = ( label_defaulted ?
+ Pango::STYLE_ITALIC :
+ Pango::STYLE_NORMAL );
+
+}
+
+void LayerSelector::_lockLayer(bool lock) {
+ if ( _layer && SP_IS_ITEM(_layer) ) {
+ SP_ITEM(_layer)->setLocked(lock);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE,
+ lock? _("Lock layer") : _("Unlock layer"));
+ }
+}
+
+void LayerSelector::_hideLayer(bool hide) {
+ if ( _layer && SP_IS_ITEM(_layer) ) {
+ SP_ITEM(_layer)->setHidden(hide);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE,
+ hide? _("Hide layer") : _("Unhide layer"));
+ }
+}
+
+} // namespace Widget
+} // 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/widget/layer-selector.h b/src/ui/widget/layer-selector.h
new file mode 100644
index 0000000..eadfce2
--- /dev/null
+++ b/src/ui/widget/layer-selector.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::LayerSelector - layer selector widget
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR
+#define SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR
+
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/liststore.h>
+#include <sigc++/slot.h>
+#include "util/list.h"
+
+class SPDesktop;
+class SPDocument;
+class SPObject;
+namespace Inkscape {
+namespace XML {
+class Node;
+}
+}
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class DocumentTreeModel;
+
+class LayerSelector : public Gtk::HBox {
+public:
+ LayerSelector(SPDesktop *desktop = nullptr);
+ ~LayerSelector() override;
+
+ SPDesktop *desktop() { return _desktop; }
+ void setDesktop(SPDesktop *desktop);
+
+private:
+ class LayerModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<unsigned> depth;
+ Gtk::TreeModelColumn<SPObject *> object;
+ Gtk::TreeModelColumn<Inkscape::XML::Node *> repr;
+ Gtk::TreeModelColumn<void *> callbacks;
+
+ LayerModelColumns() {
+ add(depth); add(object); add(repr); add(callbacks);
+ }
+ };
+
+ SPDesktop *_desktop;
+
+ Gtk::ComboBox _selector;
+ Gtk::ToggleButton _visibility_toggle;
+ Gtk::ToggleButton _lock_toggle;
+
+ LayerModelColumns _model_columns;
+ Gtk::CellRendererText _label_renderer;
+ Glib::RefPtr<Gtk::ListStore> _layer_model;
+
+// sigc::connection _desktop_shutdown_connection;
+ sigc::connection _layers_changed_connection;
+ sigc::connection _current_layer_changed_connection;
+ sigc::connection _selection_changed_connection;
+ sigc::connection _visibility_toggled_connection;
+ sigc::connection _lock_toggled_connection;
+
+ SPObject *_layer;
+
+ void _selectLayer(SPObject *layer);
+ void _layersChanged();
+
+ void _setDesktopLayer();
+
+ void _buildEntry(unsigned depth, SPObject &object);
+ void _buildEntries(unsigned depth,
+ Inkscape::Util::List<SPObject &> hierarchy);
+ void _buildSiblingEntries(unsigned depth,
+ SPObject &parent,
+ Inkscape::Util::List<SPObject &> hierarchy);
+ void _protectUpdate(sigc::slot<void> slot);
+ void _destroyEntry(Gtk::ListStore::iterator const &row);
+ void _hideLayer(bool hide);
+ void _lockLayer(bool lock);
+
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+};
+
+} // namespace Widget
+} // 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/widget/layertypeicon.cpp b/src/ui/widget/layertypeicon.cpp
new file mode 100644
index 0000000..d8b1378
--- /dev/null
+++ b/src/ui/widget/layertypeicon.cpp
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/layertypeicon.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "widgets/toolbox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+LayerTypeIcon::LayerTypeIcon() :
+ Glib::ObjectBase(typeid(LayerTypeIcon)),
+ Gtk::CellRendererPixbuf(),
+ _pixLayerName(INKSCAPE_ICON("dialog-layers")),
+ _pixGroupName(INKSCAPE_ICON("layer-duplicate")),
+ _pixPathName(INKSCAPE_ICON("layer-rename")),
+ _property_active(*this, "active", false),
+ _property_activatable(*this, "activatable", true),
+ _property_pixbuf_layer(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_group(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_path(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+{
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ _property_pixbuf_layer = sp_get_icon_pixbuf(_pixLayerName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_group = sp_get_icon_pixbuf(_pixGroupName, GTK_ICON_SIZE_MENU);
+ _property_pixbuf_path = sp_get_icon_pixbuf(_pixPathName, GTK_ICON_SIZE_MENU);
+
+ property_pixbuf() = _property_pixbuf_path.get_value();
+}
+
+void LayerTypeIcon::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void LayerTypeIcon::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void LayerTypeIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ property_pixbuf() = _property_active.get_value() == 1 ? _property_pixbuf_group : (_property_active.get_value() == 2 ? _property_pixbuf_layer : _property_pixbuf_path);
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool
+LayerTypeIcon::activate_vfunc(GdkEvent* event,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ _signal_pre_toggle.emit(event);
+ _signal_toggled.emit(path);
+
+ return false;
+}
+
+
+} // namespace Widget
+} // 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/widget/layertypeicon.h b/src/ui/widget/layertypeicon.h
new file mode 100644
index 0000000..7dccf4c
--- /dev/null
+++ b/src/ui/widget/layertypeicon.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_LAYERTYPEICON_H__
+#define __UI_DIALOG_LAYERTYPEICON_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class LayerTypeIcon : public Gtk::CellRendererPixbuf {
+public:
+ LayerTypeIcon();
+ ~LayerTypeIcon() override = default;;
+
+ sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;}
+ sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; }
+
+ Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy<int> property_activatable() { return _property_activatable.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ Glib::ustring _pixLayerName;
+ Glib::ustring _pixGroupName;
+ Glib::ustring _pixPathName;
+
+ Glib::Property<int> _property_active;
+ Glib::Property<int> _property_activatable;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_layer;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_group;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_path;
+
+ sigc::signal<void, const Glib::ustring&> _signal_toggled;
+ sigc::signal<void, GdkEvent const *> _signal_pre_toggle;
+
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/licensor.cpp b/src/ui/widget/licensor.cpp
new file mode 100644
index 0000000..2ad811f
--- /dev/null
+++ b/src/ui/widget/licensor.cpp
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "licensor.h"
+
+#include <gtkmm/entry.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/registry.h"
+#include "rdf.h"
+#include "inkscape.h"
+#include "document-undo.h"
+#include "verbs.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+const struct rdf_license_t _proprietary_license =
+ {_("Proprietary"), "", nullptr};
+
+const struct rdf_license_t _other_license =
+ {Q_("MetadataLicence|Other"), "", nullptr};
+
+class LicenseItem : public Gtk::RadioButton {
+public:
+ LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group);
+protected:
+ void on_toggled() override;
+ struct rdf_license_t const *_lic;
+ EntityEntry *_eep;
+ Registry &_wr;
+};
+
+LicenseItem::LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group)
+: Gtk::RadioButton(_(license->name)), _lic(license), _eep(entity), _wr(wr)
+{
+ if (group) {
+ set_group (*group);
+ }
+}
+
+/// \pre it is assumed that the license URI entry is a Gtk::Entry
+void LicenseItem::on_toggled()
+{
+ if (_wr.isUpdating()) return;
+
+ _wr.setUpdating (true);
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ rdf_set_license (doc, _lic->details ? _lic : nullptr);
+ if (doc->isSensitive()) {
+ DocumentUndo::done(doc, SP_VERB_NONE, _("Document license updated"));
+ }
+ _wr.setUpdating (false);
+ static_cast<Gtk::Entry*>(_eep->_packable)->set_text (_lic->uri);
+ _eep->on_changed();
+}
+
+//---------------------------------------------------
+
+Licensor::Licensor()
+: Gtk::VBox(false,4),
+ _eentry (nullptr)
+{
+}
+
+Licensor::~Licensor()
+{
+ if (_eentry) delete _eentry;
+}
+
+void Licensor::init (Registry& wr)
+{
+ /* add license-specific metadata entry areas */
+ rdf_work_entity_t* entity = rdf_find_entity ( "license_uri" );
+ _eentry = EntityEntry::create (entity, wr);
+
+ LicenseItem *i;
+ wr.setUpdating (true);
+ i = Gtk::manage (new LicenseItem (&_proprietary_license, _eentry, wr, nullptr));
+ Gtk::RadioButtonGroup group = i->get_group();
+ add (*i);
+ LicenseItem *pd = i;
+
+ for (struct rdf_license_t * license = rdf_licenses;
+ license && license->name;
+ license++) {
+ i = Gtk::manage (new LicenseItem (license, _eentry, wr, &group));
+ add(*i);
+ }
+ // add Other at the end before the URI field for the confused ppl.
+ LicenseItem *io = Gtk::manage (new LicenseItem (&_other_license, _eentry, wr, &group));
+ add (*io);
+
+ pd->set_active();
+ wr.setUpdating (false);
+
+ Gtk::HBox *box = Gtk::manage (new Gtk::HBox);
+ pack_start (*box, true, true, 0);
+
+ box->pack_start (_eentry->_label, false, false, 5);
+ box->pack_start (*_eentry->_packable, true, true, 0);
+
+ show_all_children();
+}
+
+void Licensor::update (SPDocument *doc)
+{
+ /* identify the license info */
+ struct rdf_license_t * license = rdf_get_license (doc);
+
+ if (license) {
+ int i;
+ for (i=0; rdf_licenses[i].name; i++)
+ if (license == &rdf_licenses[i])
+ break;
+ static_cast<LicenseItem*>(get_children()[i+1])->set_active();
+ }
+ else {
+ static_cast<LicenseItem*>(get_children()[0])->set_active();
+ }
+
+ /* update the URI */
+ _eentry->update (doc);
+}
+
+} // 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/widget/licensor.h b/src/ui/widget/licensor.h
new file mode 100644
index 0000000..3e1f0da
--- /dev/null
+++ b/src/ui/widget/licensor.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_LICENSOR_H
+#define INKSCAPE_UI_WIDGET_LICENSOR_H
+
+#include <gtkmm/box.h>
+
+class SPDocument;
+
+namespace Inkscape {
+ namespace UI {
+ namespace Widget {
+
+class EntityEntry;
+class Registry;
+
+
+/**
+ * Widget for specifying a document's license; part of document
+ * preferences dialog.
+ */
+class Licensor : public Gtk::VBox {
+public:
+ Licensor();
+ ~Licensor() override;
+ void init (Registry&);
+ void update (SPDocument *doc);
+
+protected:
+ EntityEntry *_eentry;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_LICENSOR_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/widget/notebook-page.cpp b/src/ui/widget/notebook-page.cpp
new file mode 100644
index 0000000..a189d78
--- /dev/null
+++ b/src/ui/widget/notebook-page.cpp
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Notebook page widget.
+ *
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "notebook-page.h"
+
+# include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+NotebookPage::NotebookPage(int n_rows, int n_columns, bool expand, bool fill, guint padding)
+ :_table(Gtk::manage(new Gtk::Grid()))
+{
+ set_name("NotebookPage");
+ set_border_width(4);
+ set_spacing(4);
+
+ _table->set_row_spacing(4);
+ _table->set_column_spacing(4);
+
+ pack_start(*_table, expand, fill, padding);
+}
+
+} // namespace Widget
+} // 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/widget/notebook-page.h b/src/ui/widget/notebook-page.h
new file mode 100644
index 0000000..cc11d30
--- /dev/null
+++ b/src/ui/widget/notebook-page.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H
+#define INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A tabbed notebook page for dialogs.
+ */
+class NotebookPage : public Gtk::VBox
+{
+public:
+
+ NotebookPage();
+
+ /**
+ * Construct a NotebookPage.
+ */
+ NotebookPage(int n_rows, int n_columns, bool expand=false, bool fill=false, guint padding=0);
+
+ Gtk::Grid& table() { return *_table; }
+
+protected:
+ Gtk::Grid *_table;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_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/widget/object-composite-settings.cpp b/src/ui/widget/object-composite-settings.cpp
new file mode 100644
index 0000000..db2e91c
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.cpp
@@ -0,0 +1,325 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A widget for controlling object compositing (filter, opacity, etc.)
+ *
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Niko Kiirala <niko@kiirala.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004--2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/object-composite-settings.h"
+
+#include "desktop.h"
+
+#include "desktop-style.h"
+#include "document.h"
+#include "document-undo.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "style.h"
+#include "svg/css-ostringstream.h"
+#include "verbs.h"
+#include "display/sp-canvas.h"
+#include "object/filters/blend.h"
+#include "ui/widget/style-subject.h"
+
+constexpr double BLUR_MULTIPLIER = 4.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ObjectCompositeSettings::ObjectCompositeSettings(unsigned int verb_code, char const *history_prefix, int flags)
+: _verb_code(verb_code),
+ _blend_tag(Glib::ustring(history_prefix) + ":blend"),
+ _blur_tag(Glib::ustring(history_prefix) + ":blur"),
+ _opacity_tag(Glib::ustring(history_prefix) + ":opacity"),
+ _isolation_tag(Glib::ustring(history_prefix) + ":isolation"),
+ _filter_modifier(flags),
+ _blocked(false)
+{
+ set_name( "ObjectCompositeSettings");
+
+ // Filter Effects
+ pack_start(_filter_modifier, false, false, 2);
+
+ _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged));
+ _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged));
+ _filter_modifier.signal_opacity_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_opacityValueChanged));
+ _filter_modifier.signal_isolation_changed().connect(
+ sigc::mem_fun(*this, &ObjectCompositeSettings::_isolationValueChanged));
+
+ show_all_children();
+}
+
+ObjectCompositeSettings::~ObjectCompositeSettings() {
+ setSubject(nullptr);
+}
+
+void ObjectCompositeSettings::setSubject(StyleSubject *subject) {
+ _subject_changed.disconnect();
+ if (subject) {
+ _subject = subject;
+ _subject_changed = _subject->connectChanged(sigc::mem_fun(*this, &ObjectCompositeSettings::_subjectChanged));
+ _subject->setDesktop(SP_ACTIVE_DESKTOP);
+ }
+}
+
+// We get away with sharing one callback for blend and blur as this is used by
+// * the Layers dialog where only one layer can be selected at a time,
+// * the Fill and Stroke dialog where only blur is used.
+// If both blend and blur are used in a dialog where more than one object can
+// be selected then this should be split into separate functions for blend and
+// blur (like in the Objects dialog).
+void
+ObjectCompositeSettings::_blendBlurValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+ SPDocument *document = desktop->getDocument();
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in crash 1580903
+ //sp_canvas_force_full_redraw_after_interruptions(desktop->getCanvas(), 0);
+
+ Geom::OptRect bbox = _subject->getBounds(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?
+ double blur_value = _filter_modifier.get_blur_value() / 100.0;
+ radius = blur_value * blur_value * perimeter / BLUR_MULTIPLIER;
+ } else {
+ radius = 0;
+ }
+
+ //apply created filter to every selected item
+ std::vector<SPObject*> sel = _subject->list();
+ for (auto i : sel) {
+ if (!SP_IS_ITEM(i)) {
+ continue;
+ }
+ SPItem * item = SP_ITEM(i);
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+ bool change_blend = (item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL) != _filter_modifier.get_blend_mode();
+ // < 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 (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
+ item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
+ } else {
+ item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode();
+ }
+
+ if (radius == 0 && item->style->filter.set
+ && filter_is_single_gaussian_blur(SP_FILTER(item->style->getFilter()))) {
+ remove_filter(item, false);
+ } else if (radius != 0) {
+ SPFilter *filter = modify_filter_gaussian_blur_from_item(document, item, radius);
+ sp_style_set_property_url(item, "filter", filter, false);
+ }
+ if (change_blend) { //we do blend so we need update display style
+ item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ } else {
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::maybeDone(document, _blur_tag.c_str(), _verb_code,
+ _("Change blur/blend filter"));
+
+ // resume interruptibility
+ //sp_canvas_end_forced_full_redraws(desktop->getCanvas());
+
+ _blocked = false;
+}
+
+void
+ObjectCompositeSettings::_opacityValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ Inkscape::CSSOStringStream os;
+ os << CLAMP (_filter_modifier.get_opacity_value() / 100, 0.0, 1.0);
+ sp_repr_css_set_property (css, "opacity", os.str().c_str());
+
+ _subject->setCSS(css);
+
+ sp_repr_css_attr_unref (css);
+
+ DocumentUndo::maybeDone(desktop->getDocument(), _opacity_tag.c_str(), _verb_code,
+ _("Change opacity"));
+
+ // resume interruptibility
+ //sp_canvas_end_forced_full_redraws(desktop->getCanvas());
+
+ _blocked = false;
+}
+
+void ObjectCompositeSettings::_isolationValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ for (auto item : _subject->list()) {
+ 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;
+ }
+ item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ }
+
+ DocumentUndo::maybeDone(desktop->getDocument(), _isolation_tag.c_str(), _verb_code, _("Change isolation"));
+
+ // resume interruptibility
+ // sp_canvas_end_forced_full_redraws(desktop->getCanvas());
+
+ _blocked = false;
+}
+
+void
+ObjectCompositeSettings::_subjectChanged() {
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+ SPStyle query(desktop->getDocument());
+ int result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_MASTEROPACITY);
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_opacity_value(100 * SP_SCALE24_TO_FLOAT(query.opacity.value));
+ break;
+ }
+
+ //query now for current filter mode and average blurring of selection
+ const int isolation_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_ISOLATION);
+ switch (isolation_result) {
+ case QUERY_STYLE_NOTHING:
+ _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_isolation_mode(query.isolation.value, true); // here dont work mix_blend_mode.set
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false);
+ // TODO: set text
+ break;
+ }
+
+ // query now for current filter mode and average blurring of selection
+ const int blend_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLEND);
+ switch(blend_result) {
+ case QUERY_STYLE_NOTHING:
+ _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_blend_mode(query.mix_blend_mode.value, true); // here dont work mix_blend_mode.set
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
+ break;
+ }
+
+ int blur_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLUR);
+ switch (blur_result) {
+ case QUERY_STYLE_NOTHING: // no blurring
+ _filter_modifier.set_blur_value(0);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX);
+ if (bbox) {
+ double perimeter =
+ bbox->dimensions()[Geom::X] +
+ bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct?
+ // update blur widget value
+ float radius = query.filter_gaussianBlur_deviation.value;
+ float percent = std::sqrt(radius * BLUR_MULTIPLIER / perimeter) * 100;
+ _filter_modifier.set_blur_value(percent);
+ }
+ break;
+ }
+
+ // If we have nothing selected, disable dialog.
+ if (result == QUERY_STYLE_NOTHING &&
+ blend_result == QUERY_STYLE_NOTHING ) {
+ _filter_modifier.set_sensitive( false );
+ } else {
+ _filter_modifier.set_sensitive( true );
+ }
+
+ _blocked = 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/widget/object-composite-settings.h b/src/ui/widget/object-composite-settings.h
new file mode 100644
index 0000000..9650118
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H
+#define SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H
+
+/*
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2004--2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/label.h>
+#include <gtkmm/scale.h>
+#include <glibmm/ustring.h>
+
+#include "ui/widget/filter-effect-chooser.h"
+
+class SPDesktop;
+struct InkscapeApplication;
+
+namespace Inkscape {
+
+namespace UI {
+namespace Widget {
+
+class StyleSubject;
+
+/*
+ * A widget for controlling object compositing (filter, opacity, etc.)
+ */
+class ObjectCompositeSettings : public Gtk::VBox {
+public:
+ ObjectCompositeSettings(unsigned int verb_code, char const *history_prefix, int flags);
+ ~ObjectCompositeSettings() override;
+
+ void setSubject(StyleSubject *subject);
+
+private:
+ unsigned int _verb_code;
+ Glib::ustring _blend_tag;
+ Glib::ustring _blur_tag;
+ Glib::ustring _opacity_tag;
+ Glib::ustring _isolation_tag;
+
+ StyleSubject *_subject;
+
+ SimpleFilterModifier _filter_modifier;
+
+ bool _blocked;
+ gulong _desktop_activated;
+ sigc::connection _subject_changed;
+
+ static void _on_desktop_activate(SPDesktop *desktop, ObjectCompositeSettings *w);
+ static void _on_desktop_deactivate(SPDesktop *desktop, ObjectCompositeSettings *w);
+ void _subjectChanged();
+ void _blendBlurValueChanged();
+ void _opacityValueChanged();
+ void _isolationValueChanged();
+};
+
+}
+}
+}
+
+#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/widget/page-sizer.cpp b/src/ui/widget/page-sizer.cpp
new file mode 100644
index 0000000..d869a1c
--- /dev/null
+++ b/src/ui/widget/page-sizer.cpp
@@ -0,0 +1,781 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ *
+ * Paper-size widget and helper functions
+ */
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Bob Jamison <ishmal@users.sf.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-sizer.h"
+#include "pages-skeleton.h"
+#include <glib.h>
+#include <glibmm/i18n.h>
+#include "verbs.h"
+#include "helper/action.h"
+#include "object/sp-root.h"
+#include "io/resource.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+//########################################################################
+//# P A G E S I Z E R
+//########################################################################
+
+/**
+ * Constructor
+ */
+PageSizer::PageSizer(Registry & _wr)
+ : Gtk::VBox(false,4),
+ _dimensionUnits( _("U_nits:"), "units", _wr ),
+ _dimensionWidth( _("_Width:"), _("Width of paper"), "width", _dimensionUnits, _wr ),
+ _dimensionHeight( _("_Height:"), _("Height of paper"), "height", _dimensionUnits, _wr ),
+ _marginLock( _("Loc_k margins"), _("Lock margins"), "lock-margins", _wr, false, nullptr, nullptr),
+ _lock_icon(),
+ _marginTop( _("T_op:"), _("Top margin"), "fit-margin-top", _wr ),
+ _marginLeft( _("L_eft:"), _("Left margin"), "fit-margin-left", _wr),
+ _marginRight( _("Ri_ght:"), _("Right margin"), "fit-margin-right", _wr),
+ _marginBottom( _("Botto_m:"), _("Bottom margin"), "fit-margin-bottom", _wr),
+ _lockMarginUpdate(false),
+ _scaleX(_("Scale _x:"), _("Scale X"), "scale-x", _wr),
+ _scaleY(_("Scale _y:"), _("While SVG allows non-uniform scaling it is recommended to use only uniform scaling in Inkscape. To set a non-uniform scaling, set the 'viewBox' directly."), "scale-y", _wr),
+ _lockScaleUpdate(false),
+ _viewboxX(_("X:"), _("X"), "viewbox-x", _wr),
+ _viewboxY(_("Y:"), _("Y"), "viewbox-y", _wr),
+ _viewboxW(_("Width:"), _("Width"), "viewbox-width", _wr),
+ _viewboxH(_("Height:"), _("Height"), "viewbox-height", _wr),
+ _lockViewboxUpdate(false),
+ _widgetRegistry(&_wr)
+{
+ // set precision of scalar entry boxes
+ _wr.setUpdating (true);
+ _dimensionWidth.setDigits(5);
+ _dimensionHeight.setDigits(5);
+ _marginTop.setDigits(5);
+ _marginLeft.setDigits(5);
+ _marginRight.setDigits(5);
+ _marginBottom.setDigits(5);
+ _scaleX.setDigits(5);
+ _scaleY.setDigits(5);
+ _viewboxX.setDigits(5);
+ _viewboxY.setDigits(5);
+ _viewboxW.setDigits(5);
+ _viewboxH.setDigits(5);
+ _dimensionWidth.setRange( 0.00001, 10000000 );
+ _dimensionHeight.setRange( 0.00001, 10000000 );
+ _scaleX.setRange( 0.00001, 100000 );
+ _scaleY.setRange( 0.00001, 100000 );
+ _viewboxX.setRange( -10000000, 10000000 );
+ _viewboxY.setRange( -10000000, 10000000 );
+ _viewboxW.setRange( 0.00001, 10000000 );
+ _viewboxH.setRange( 0.00001, 10000000 );
+
+ _scaleY.set_sensitive (false); // We only want to display Y scale.
+
+ _wr.setUpdating (false);
+
+ //# Set up the Paper Size combo box
+ _paperSizeListStore = Gtk::ListStore::create(_paperSizeListColumns);
+ _paperSizeList.set_model(_paperSizeListStore);
+ _paperSizeList.append_column(_("Name"),
+ _paperSizeListColumns.nameColumn);
+ _paperSizeList.append_column(_("Description"),
+ _paperSizeListColumns.descColumn);
+ _paperSizeList.set_headers_visible(false);
+ _paperSizeListSelection = _paperSizeList.get_selection();
+ _paper_size_list_connection =
+ _paperSizeListSelection->signal_changed().connect (
+ sigc::mem_fun (*this, &PageSizer::on_paper_size_list_changed));
+ _paperSizeListScroller.add(_paperSizeList);
+ _paperSizeListScroller.set_shadow_type(Gtk::SHADOW_IN);
+ _paperSizeListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _paperSizeListScroller.set_size_request(-1, 130);
+
+
+ char *path = Inkscape::IO::Resource::profile_path("pages.csv");
+ if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
+ if (!g_file_set_contents(path, pages_skeleton, -1, nullptr)) {
+ g_warning("%s", _("Failed to create the page file."));
+ }
+ }
+
+ gchar *content = nullptr;
+ if (g_file_get_contents(path, &content, nullptr, nullptr)) {
+
+ gchar **lines = g_strsplit_set(content, "\n", 0);
+
+ for (int i = 0; lines && lines[i]; ++i) {
+ gchar **line = g_strsplit_set(lines[i], ",", 5);
+ if (!line[0] || !line[1] || !line[2] || !line[3] || line[0][0]=='#')
+ continue;
+ //name, width, height, unit
+ double width = g_ascii_strtod(line[1], nullptr);
+ double height = g_ascii_strtod(line[2], nullptr);
+ g_strstrip(line[0]);
+ g_strstrip(line[3]);
+ Glib::ustring name = line[0];
+ char formatBuf[80];
+ snprintf(formatBuf, 79, "%0.1f x %0.1f", width, height);
+ Glib::ustring desc = formatBuf;
+ desc.append(" " + std::string(line[3]));
+ PaperSize paper(name, width, height, Inkscape::Util::unit_table.getUnit(line[3]));
+ _paperSizeTable[name] = paper;
+ Gtk::TreeModel::Row row = *(_paperSizeListStore->append());
+ row[_paperSizeListColumns.nameColumn] = name;
+ row[_paperSizeListColumns.descColumn] = desc;
+ g_strfreev(line);
+ }
+ g_strfreev(lines);
+ g_free(content);
+ }
+ g_free(path);
+
+ pack_start (_paperSizeListScroller, true, true, 0);
+
+ //## Set up orientation radio buttons
+ pack_start (_orientationBox, false, false, 0);
+ _orientationLabel.set_label(_("Orientation:"));
+ _orientationBox.pack_start(_orientationLabel, false, false, 0);
+ _landscapeButton.set_use_underline();
+ _landscapeButton.set_label(_("_Landscape"));
+ _landscapeButton.set_active(true);
+ Gtk::RadioButton::Group group = _landscapeButton.get_group();
+ _orientationBox.pack_end (_landscapeButton, false, false, 5);
+ _portraitButton.set_use_underline();
+ _portraitButton.set_label(_("_Portrait"));
+ _portraitButton.set_active(true);
+ _orientationBox.pack_end (_portraitButton, false, false, 5);
+ _portraitButton.set_group (group);
+ _portraitButton.set_active (true);
+
+ // Setting default custom unit to document unit
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ SPNamedView *nv = dt->getNamedView();
+ _wr.setUpdating (true);
+ if (nv->page_size_units) {
+ _dimensionUnits.setUnit(nv->page_size_units->abbr);
+ } else if (nv->display_units) {
+ _dimensionUnits.setUnit(nv->display_units->abbr);
+ }
+ _wr.setUpdating (false);
+
+
+ //## Set up custom size frame
+ _customFrame.set_label(_("Custom size"));
+ pack_start (_customFrame, false, false, 0);
+ _customFrame.add(_customDimTable);
+
+ _customDimTable.set_border_width(4);
+ _customDimTable.set_row_spacing(4);
+ _customDimTable.set_column_spacing(4);
+
+ _dimensionHeight.set_halign(Gtk::ALIGN_CENTER);
+ _dimensionUnits.set_halign(Gtk::ALIGN_END);
+ _customDimTable.attach(_dimensionWidth, 0, 0, 1, 1);
+ _customDimTable.attach(_dimensionHeight, 1, 0, 1, 1);
+ _customDimTable.attach(_dimensionUnits, 2, 0, 1, 1);
+
+ _customDimTable.attach(_fitPageMarginExpander, 0, 1, 3, 1);
+
+ //## Set up fit page expander
+ _fitPageMarginExpander.set_use_underline();
+ _fitPageMarginExpander.set_label(_("Resi_ze page to content..."));
+ _fitPageMarginExpander.add(_marginTable);
+
+ _marginTable.set_border_width(4);
+ _marginTable.set_row_spacing(4);
+ _marginTable.set_column_spacing(4);
+
+ //### margin label and lock button
+ _marginLabel.set_markup(Glib::ustring("<b><i>") + _("Margins") + "</i></b>");
+ _marginLabel.set_halign(Gtk::ALIGN_CENTER);
+
+ _lock_icon.set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _lock_icon.show();
+ _marginLock.set_active(false);
+ _marginLock.add(_lock_icon);
+
+ _marginBox.set_spacing(4);
+ _marginBox.add(_marginLabel);
+ _marginBox.add(_marginLock);
+ _marginBox.set_halign(Gtk::ALIGN_CENTER);
+ _marginTable.attach(_marginBox, 1, 1, 1, 1);
+
+ //### margins
+ _marginTop.set_halign(Gtk::ALIGN_CENTER);
+ _marginLeft.set_halign(Gtk::ALIGN_START);
+ _marginRight.set_halign(Gtk::ALIGN_END);
+ _marginBottom.set_halign(Gtk::ALIGN_CENTER);
+
+ _marginTable.attach(_marginTop, 0, 0, 3, 1);
+ _marginTable.attach(_marginLeft, 0, 1, 1, 1);
+ _marginTable.attach(_marginRight, 2, 1, 1, 1);
+ _marginTable.attach(_marginBottom, 0, 2, 3, 1);
+
+ //### fit page to drawing button
+ _fitPageButton.set_use_underline();
+ _fitPageButton.set_label(_("_Resize page to drawing or selection (Ctrl+Shift+R)"));
+ _fitPageButton.set_tooltip_text(_("Resize the page to fit the current selection, or the entire drawing if there is no selection"));
+
+ _fitPageButton.set_hexpand();
+ _fitPageButton.set_halign(Gtk::ALIGN_CENTER);
+ _marginTable.attach(_fitPageButton, 0, 3, 3, 1);
+
+
+ //## Set up scale frame
+ _scaleFrame.set_label(_("Scale"));
+ pack_start (_scaleFrame, false, false, 0);
+ _scaleFrame.add(_scaleTable);
+
+ _scaleTable.set_border_width(4);
+ _scaleTable.set_row_spacing(4);
+ _scaleTable.set_column_spacing(4);
+
+ _scaleTable.attach(_scaleX, 0, 0, 1, 1);
+ _scaleTable.attach(_scaleY, 1, 0, 1, 1);
+ _scaleTable.attach(_scaleLabel, 2, 0, 1, 1);
+
+ _viewboxExpander.set_hexpand();
+ _scaleTable.attach(_viewboxExpander, 0, 2, 3, 1);
+
+ _viewboxExpander.set_use_underline();
+ _viewboxExpander.set_label(_("_Viewbox..."));
+ _viewboxExpander.add(_viewboxTable);
+
+ _viewboxTable.set_border_width(4);
+ _viewboxTable.set_row_spacing(4);
+ _viewboxTable.set_column_spacing(4);
+
+ _viewboxX.set_halign(Gtk::ALIGN_END);
+ _viewboxY.set_halign(Gtk::ALIGN_END);
+ _viewboxW.set_halign(Gtk::ALIGN_END);
+ _viewboxH.set_halign(Gtk::ALIGN_END);
+ _viewboxSpacer.set_hexpand();
+ _viewboxTable.attach(_viewboxX, 0, 0, 1, 1);
+ _viewboxTable.attach(_viewboxY, 1, 0, 1, 1);
+ _viewboxTable.attach(_viewboxW, 0, 1, 1, 1);
+ _viewboxTable.attach(_viewboxH, 1, 1, 1, 1);
+ _viewboxTable.attach(_viewboxSpacer, 2, 0, 3, 1);
+
+ _wr.setUpdating (true);
+ updateScaleUI();
+ _wr.setUpdating (false);
+}
+
+
+/**
+ * Destructor
+ */
+PageSizer::~PageSizer()
+= default;
+
+
+
+/**
+ * Initialize or reset this widget
+ */
+void
+PageSizer::init ()
+{
+ _landscape_connection = _landscapeButton.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_landscape));
+ _portrait_connection = _portraitButton.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_portrait));
+ _changedw_connection = _dimensionWidth.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_value_changed));
+ _changedh_connection = _dimensionHeight.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_value_changed));
+ _changedu_connection = _dimensionUnits.getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &PageSizer::on_units_changed));
+ _fitPageButton.signal_clicked().connect(sigc::mem_fun(*this, &PageSizer::fire_fit_canvas_to_selection_or_drawing));
+ _changeds_connection = _scaleX.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_scale_changed));
+ _changedvx_connection = _viewboxX.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed));
+ _changedvy_connection = _viewboxY.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed));
+ _changedvw_connection = _viewboxW.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed));
+ _changedvh_connection = _viewboxH.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed));
+ _changedlk_connection = _marginLock.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_margin_lock_changed));
+ _changedmt_connection = _marginTop.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginTop));
+ _changedmb_connection = _marginBottom.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginBottom));
+ _changedml_connection = _marginLeft.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginLeft));
+ _changedmr_connection = _marginRight.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginRight));
+ show_all_children();
+}
+
+
+/**
+ * Set document dimensions (if not called by Doc prop's update()) and
+ * set the PageSizer's widgets and text entries accordingly. If
+ * 'changeList' is true, then adjust the paperSizeList to show the closest
+ * standard page size.
+ *
+ * \param w, h
+ * \param changeList whether to modify the paper size list
+ */
+void
+PageSizer::setDim (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h, bool changeList, bool changeSize)
+{
+ static bool _called = false;
+ if (_called) {
+ return;
+ }
+
+ _called = true;
+
+ _paper_size_list_connection.block();
+ _landscape_connection.block();
+ _portrait_connection.block();
+ _changedw_connection.block();
+ _changedh_connection.block();
+
+ _unit = w.unit->abbr;
+
+ if (SP_ACTIVE_DESKTOP && !_widgetRegistry->isUpdating()) {
+ SPDocument *doc = SP_ACTIVE_DESKTOP->getDocument();
+ Inkscape::Util::Quantity const old_height = doc->getHeight();
+ doc->setWidthAndHeight (w, h, changeSize);
+ // The origin for the user is in the lower left corner; this point should remain stationary when
+ // changing the page size. The SVG's origin however is in the upper left corner, so we must compensate for this
+ if (changeSize && !doc->is_yaxisdown()) {
+ Geom::Translate const vert_offset(Geom::Point(0, (old_height.value("px") - h.value("px"))));
+ doc->getRoot()->translateChildItems(vert_offset);
+ }
+ DocumentUndo::done(doc, SP_VERB_NONE, _("Set page size"));
+ }
+
+ if ( w != h ) {
+ _landscapeButton.set_sensitive(true);
+ _portraitButton.set_sensitive (true);
+ _landscape = ( w > h );
+ _landscapeButton.set_active(_landscape ? true : false);
+ _portraitButton.set_active (_landscape ? false : true);
+ } else {
+ _landscapeButton.set_sensitive(false);
+ _portraitButton.set_sensitive (false);
+ }
+
+ if (changeList)
+ {
+ Gtk::TreeModel::Row row = (*find_paper_size(w, h));
+ if (row)
+ _paperSizeListSelection->select(row);
+ }
+
+ _dimensionWidth.setUnit(w.unit->abbr);
+ _dimensionWidth.setValue (w.quantity);
+ _dimensionHeight.setUnit(h.unit->abbr);
+ _dimensionHeight.setValue (h.quantity);
+
+
+ _paper_size_list_connection.unblock();
+ _landscape_connection.unblock();
+ _portrait_connection.unblock();
+ _changedw_connection.unblock();
+ _changedh_connection.unblock();
+
+ _called = false;
+}
+
+/**
+ * Updates the scalar widgets for the fit margins. (Just changes the value
+ * of the ui widgets to match the xml).
+ */
+void
+PageSizer::updateFitMarginsUI(Inkscape::XML::Node *nv_repr)
+{
+ if (!_lockMarginUpdate) {
+ double value = 0.0;
+ if (sp_repr_get_double(nv_repr, "fit-margin-top", &value)) {
+ _marginTop.setValue(value);
+ }
+ if (sp_repr_get_double(nv_repr, "fit-margin-left", &value)) {
+ _marginLeft.setValue(value);
+ }
+ if (sp_repr_get_double(nv_repr, "fit-margin-right", &value)) {
+ _marginRight.setValue(value);
+ }
+ if (sp_repr_get_double(nv_repr, "fit-margin-bottom", &value)) {
+ _marginBottom.setValue(value);
+ }
+ }
+}
+
+
+/**
+ * Returns an iterator pointing to a row in paperSizeListStore which
+ * contains a paper of the specified size, or
+ * paperSizeListStore->children().end() if no such paper exists.
+ *
+ * The code is not tested for the case where w and h have different units.
+ */
+Gtk::ListStore::iterator
+PageSizer::find_paper_size (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h) const
+{
+ // The code below assumes that w < h, so make sure that's the case:
+ if ( h < w ) {
+ std::swap(h,w);
+ }
+
+ std::map<Glib::ustring, PaperSize>::const_iterator iter;
+ for (iter = _paperSizeTable.begin() ;
+ iter != _paperSizeTable.end() ; ++iter) {
+ PaperSize paper = iter->second;
+ Inkscape::Util::Quantity smallX (paper.smaller, paper.unit);
+ Inkscape::Util::Quantity largeX (paper.larger, paper.unit);
+
+ // account for landscape formats (e.g. business cards)
+ if (largeX < smallX) {
+ std::swap(largeX, smallX);
+ }
+
+ if ( are_near(w, smallX, 0.1) && are_near(h, largeX, 0.1) ) {
+ Gtk::ListStore::iterator p = _paperSizeListStore->children().begin();
+ Gtk::ListStore::iterator pend = _paperSizeListStore->children().end();
+ // We need to search paperSizeListStore explicitly for the
+ // specified paper size because it is sorted in a different
+ // way than paperSizeTable (which is sorted alphabetically)
+ for ( ; p != pend; ++p) {
+ if ((*p)[_paperSizeListColumns.nameColumn] == paper.name) {
+ return p;
+ }
+ }
+ }
+ }
+ return _paperSizeListStore->children().end();
+}
+
+
+
+/**
+ * Tell the desktop to fit the page size to the selection or drawing.
+ */
+void
+PageSizer::fire_fit_canvas_to_selection_or_drawing()
+{
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (!dt) {
+ return;
+ }
+ SPDocument *doc;
+ SPNamedView *nv;
+ Inkscape::XML::Node *nv_repr;
+
+ if ((doc = SP_ACTIVE_DESKTOP->getDocument())
+ && (nv = sp_document_namedview(doc, nullptr))
+ && (nv_repr = nv->getRepr())) {
+ _lockMarginUpdate = true;
+ sp_repr_set_svg_double(nv_repr, "fit-margin-top", _marginTop.getValue());
+ sp_repr_set_svg_double(nv_repr, "fit-margin-left", _marginLeft.getValue());
+ sp_repr_set_svg_double(nv_repr, "fit-margin-right", _marginRight.getValue());
+ sp_repr_set_svg_double(nv_repr, "fit-margin-bottom", _marginBottom.getValue());
+ _lockMarginUpdate = false;
+ }
+
+ Verb *verb = Verb::get( SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING );
+ if (verb) {
+ SPAction *action = verb->get_action(Inkscape::ActionContext(dt));
+ if (action) {
+ sp_action_perform(action, nullptr);
+ }
+ }
+}
+
+
+
+/**
+ * Paper Size list callback for when a user changes the selection
+ */
+void
+PageSizer::on_paper_size_list_changed()
+{
+ //Glib::ustring name = _paperSizeList.get_active_text();
+ Gtk::TreeModel::iterator miter = _paperSizeListSelection->get_selected();
+ if(!miter)
+ {
+ //error?
+ return;
+ }
+ Gtk::TreeModel::Row row = *miter;
+ Glib::ustring name = row[_paperSizeListColumns.nameColumn];
+ std::map<Glib::ustring, PaperSize>::const_iterator piter =
+ _paperSizeTable.find(name);
+ if (piter == _paperSizeTable.end()) {
+ g_warning("paper size '%s' not found in table", name.c_str());
+ return;
+ }
+ PaperSize paper = piter->second;
+ Inkscape::Util::Quantity w = Inkscape::Util::Quantity(paper.smaller, paper.unit);
+ Inkscape::Util::Quantity h = Inkscape::Util::Quantity(paper.larger, paper.unit);
+
+ if ( w > h ) {
+ // enforce landscape mode if this is desired for the given page format
+ _landscape = true;
+ } else {
+ // otherwise we keep the current mode
+ _landscape = _landscapeButton.get_active();
+ }
+
+ if ((_landscape && (w < h)) || (!_landscape && (w > h)))
+ setDim (h, w, false);
+ else
+ setDim (w, h, false);
+
+}
+
+
+/**
+ * Portrait button callback
+ */
+void
+PageSizer::on_portrait()
+{
+ if (!_portraitButton.get_active())
+ return;
+ Inkscape::Util::Quantity w = Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionWidth.getUnit());
+ Inkscape::Util::Quantity h = Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionHeight.getUnit());
+ if (h < w) {
+ setDim (h, w);
+ }
+}
+
+
+/**
+ * Landscape button callback
+ */
+void
+PageSizer::on_landscape()
+{
+ if (!_landscapeButton.get_active())
+ return;
+ Inkscape::Util::Quantity w = Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionWidth.getUnit());
+ Inkscape::Util::Quantity h = Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionHeight.getUnit());
+ if (w < h) {
+ setDim (h, w);
+ }
+}
+
+
+/**
+ * Update scale widgets
+ */
+void
+PageSizer::updateScaleUI()
+{
+
+ static bool _called = false;
+ if (_called) {
+ return;
+ }
+
+ _called = true;
+
+ _changeds_connection.block();
+ _changedvx_connection.block();
+ _changedvy_connection.block();
+ _changedvw_connection.block();
+ _changedvh_connection.block();
+
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (dt) {
+ SPDocument *doc = dt->getDocument();
+
+ // Update scale
+ Geom::Scale scale = doc->getDocumentScale();
+ SPNamedView *nv = dt->getNamedView();
+
+ std::stringstream ss;
+ ss << _("User units per ") << nv->display_units->abbr << "." ;
+ _scaleLabel.set_text( ss.str() );
+
+ if( !_lockScaleUpdate ) {
+
+ double scaleX_inv =
+ Inkscape::Util::Quantity::convert( scale[Geom::X], "px", nv->display_units );
+ if( scaleX_inv > 0 ) {
+ _scaleX.setValue(1.0/scaleX_inv);
+ } else {
+ // Should never happen
+ std::cerr << "PageSizer::updateScaleUI(): Invalid scale value: " << scaleX_inv << std::endl;
+ _scaleX.setValue(1.0);
+ }
+ }
+
+ { // Don't need to lock as scaleY widget not linked to callback.
+ double scaleY_inv =
+ Inkscape::Util::Quantity::convert( scale[Geom::Y], "px", nv->display_units );
+ if( scaleY_inv > 0 ) {
+ _scaleY.setValue(1.0/scaleY_inv);
+ } else {
+ // Should never happen
+ std::cerr << "PageSizer::updateScaleUI(): Invalid scale value: " << scaleY_inv << std::endl;
+ _scaleY.setValue(1.0);
+ }
+ }
+
+ if( !_lockViewboxUpdate ) {
+ Geom::Rect viewBox = doc->getViewBox();
+ _viewboxX.setValue( viewBox.min()[Geom::X] );
+ _viewboxY.setValue( viewBox.min()[Geom::Y] );
+ _viewboxW.setValue( viewBox.width() );
+ _viewboxH.setValue( viewBox.height() );
+ }
+
+ } else {
+ // Should never happen
+ std::cerr << "PageSizer::updateScaleUI(): No active desktop." << std::endl;
+ _scaleLabel.set_text( "Unknown scale" );
+ }
+
+ _changeds_connection.unblock();
+ _changedvx_connection.unblock();
+ _changedvy_connection.unblock();
+ _changedvw_connection.unblock();
+ _changedvh_connection.unblock();
+
+ _called = false;
+}
+
+
+/**
+ * Callback for the dimension widgets
+ */
+void
+PageSizer::on_value_changed()
+{
+ if (_widgetRegistry->isUpdating()) return;
+ if (_unit != _dimensionUnits.getUnit()->abbr) return;
+ setDim (Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionUnits.getUnit()),
+ Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionUnits.getUnit()));
+}
+
+void
+PageSizer::on_units_changed()
+{
+ if (_widgetRegistry->isUpdating()) return;
+ _unit = _dimensionUnits.getUnit()->abbr;
+ setDim (Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionUnits.getUnit()),
+ Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionUnits.getUnit()),
+ true, false);
+}
+
+/**
+ * Callback for scale widgets
+ */
+void
+PageSizer::on_scale_changed()
+{
+ if (_widgetRegistry->isUpdating()) return;
+
+ double value = _scaleX.getValue();
+ if( value > 0 ) {
+
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (dt) {
+ SPDocument *doc = dt->getDocument();
+ SPNamedView *nv = dt->getNamedView();
+
+ double scaleX_inv = Inkscape::Util::Quantity(1.0/value, nv->display_units ).value("px");
+
+ _lockScaleUpdate = true;
+ doc->setDocumentScale( 1.0/scaleX_inv );
+ updateScaleUI();
+ _lockScaleUpdate = false;
+ DocumentUndo::done(doc, SP_VERB_NONE, _("Set page scale"));
+ }
+ }
+}
+
+/**
+ * Callback for viewbox widgets
+ */
+void
+PageSizer::on_viewbox_changed()
+{
+ if (_widgetRegistry->isUpdating()) return;
+
+ double viewboxX = _viewboxX.getValue();
+ double viewboxY = _viewboxY.getValue();
+ double viewboxW = _viewboxW.getValue();
+ double viewboxH = _viewboxH.getValue();
+
+ if( viewboxW > 0 && viewboxH > 0) {
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (dt) {
+ SPDocument *doc = dt->getDocument();
+ _lockViewboxUpdate = true;
+ doc->setViewBox( Geom::Rect::from_xywh( viewboxX, viewboxY, viewboxW, viewboxH ) );
+ updateScaleUI();
+ _lockViewboxUpdate = false;
+ DocumentUndo::done(doc, SP_VERB_NONE, _("Set 'viewBox'"));
+ }
+ } else {
+ std::cerr
+ << "PageSizer::on_viewbox_changed(): width and height must both be greater than zero."
+ << std::endl;
+ }
+}
+
+void
+PageSizer::on_margin_lock_changed()
+{
+ if (_marginLock.get_active()) {
+ _lock_icon.set_from_icon_name("object-locked", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ double left = _marginLeft.getValue();
+ double right = _marginRight.getValue();
+ double top = _marginTop.getValue();
+ //double bottom = _marginBottom.getValue();
+ if (Geom::are_near(left,right)) {
+ if (Geom::are_near(left, top)) {
+ on_margin_changed(&_marginBottom);
+ } else {
+ on_margin_changed(&_marginTop);
+ }
+ } else {
+ if (Geom::are_near(left, top)) {
+ on_margin_changed(&_marginRight);
+ } else {
+ on_margin_changed(&_marginLeft);
+ }
+ }
+ } else {
+ _lock_icon.set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ }
+}
+
+void
+PageSizer::on_margin_changed(RegisteredScalar* widg)
+{
+ double value = widg->getValue();
+ if (_widgetRegistry->isUpdating()) return;
+ if (_marginLock.get_active() && !_lockMarginUpdate) {
+ _lockMarginUpdate = true;
+ _marginLeft.setValue(value);
+ _marginRight.setValue(value);
+ _marginTop.setValue(value);
+ _marginBottom.setValue(value);
+ _lockMarginUpdate = false;
+ }
+}
+
+} // namespace Widget
+} // 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/widget/page-sizer.h b/src/ui/widget/page-sizer.h
new file mode 100644
index 0000000..b399835
--- /dev/null
+++ b/src/ui/widget/page-sizer.h
@@ -0,0 +1,307 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005-2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_PAGE_SIZER_H
+#define INKSCAPE_UI_WIDGET_PAGE_SIZER_H
+
+#include <cstddef>
+#include "ui/widget/registered-widget.h"
+#include <sigc++/sigc++.h>
+
+#include "util/units.h"
+
+#include <gtkmm/expander.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/radiobutton.h>
+
+namespace Inkscape {
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+/**
+ * Data class used to store common paper dimensions. Used to make
+ * PageSizer's _paperSizeTable.
+ */
+class PaperSize
+{
+public:
+
+ /**
+ * Default constructor
+ */
+ PaperSize()
+ { init(); }
+
+ /**
+ * Main constructor. Use this one.
+ */
+ PaperSize(const Glib::ustring &nameArg,
+ double smallerArg,
+ double largerArg,
+ Inkscape::Util::Unit const *unitArg)
+ {
+ name = nameArg;
+ smaller = smallerArg;
+ larger = largerArg;
+ unit = unitArg;
+ }
+
+ /**
+ * Copy constructor
+ */
+ PaperSize(const PaperSize &other)
+ { assign(other); }
+
+ /**
+ * Assignment operator
+ */
+ PaperSize &operator=(const PaperSize &other)
+ { assign(other); return *this; }
+
+ /**
+ * Destructor
+ */
+ virtual ~PaperSize()
+ = default;
+
+ /**
+ * Name of this paper specification
+ */
+ Glib::ustring name;
+
+ /**
+ * The lesser of the two dimensions
+ */
+ double smaller;
+
+ /**
+ * The greater of the two dimensions
+ */
+ double larger;
+
+ /**
+ * The units (px, pt, mm, etc) of this specification
+ */
+ Inkscape::Util::Unit const *unit; /// pointer to object in UnitTable, do not delete
+
+private:
+
+ void init()
+ {
+ name = "";
+ smaller = 0.0;
+ larger = 0.0;
+ unit = unit_table.getUnit("px");
+ }
+
+ void assign(const PaperSize &other)
+ {
+ name = other.name;
+ smaller = other.smaller;
+ larger = other.larger;
+ unit = other.unit;
+ }
+
+};
+
+
+
+
+
+/**
+ * A compound widget that allows the user to select the desired
+ * page size. This widget is used in DocumentPreferences
+ */
+class PageSizer : public Gtk::VBox
+{
+public:
+
+ /**
+ * Constructor
+ */
+ PageSizer(Registry & _wr);
+
+ /**
+ * Destructor
+ */
+ ~PageSizer() override;
+
+ /**
+ * Set up or reset this widget
+ */
+ void init ();
+
+ /**
+ * Set the page size to the given dimensions. If 'changeList' is
+ * true, then reset the paper size list to the closest match
+ */
+ void setDim (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h, bool changeList=true, bool changeSize=true);
+
+ /**
+ * Updates the scalar widgets for the fit margins. (Just changes the value
+ * of the ui widgets to match the xml).
+ */
+ void updateFitMarginsUI(Inkscape::XML::Node *nv_repr);
+
+ /**
+ * Updates the margin widgets. If lock widget is active
+ */
+ void on_margin_changed(RegisteredScalar* widg);
+
+ void on_margin_lock_changed();
+
+ /**
+ * Updates the scale widgets. (Just changes the values of the ui widgets.)
+ */
+ void updateScaleUI();
+
+protected:
+
+ /**
+ * Our handy table of all 'standard' paper sizes.
+ */
+ std::map<Glib::ustring, PaperSize> _paperSizeTable;
+
+ /**
+ * Find the closest standard paper size in the table, to the
+ */
+ Gtk::ListStore::iterator find_paper_size (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h) const;
+
+ void fire_fit_canvas_to_selection_or_drawing();
+
+ //### The Paper Size selection list
+ Gtk::HBox _paperSizeListBox;
+ Gtk::Label _paperSizeListLabel;
+ class PaperSizeColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ PaperSizeColumns()
+ { add(nameColumn); add(descColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> nameColumn;
+ Gtk::TreeModelColumn<Glib::ustring> descColumn;
+ };
+
+ PaperSizeColumns _paperSizeListColumns;
+ Glib::RefPtr<Gtk::ListStore> _paperSizeListStore;
+ Gtk::TreeView _paperSizeList;
+ Glib::RefPtr<Gtk::TreeSelection> _paperSizeListSelection;
+ Gtk::ScrolledWindow _paperSizeListScroller;
+ //callback
+ void on_paper_size_list_changed();
+ sigc::connection _paper_size_list_connection;
+
+ //### Portrait or landscape orientation
+ Gtk::HBox _orientationBox;
+ Gtk::Label _orientationLabel;
+ Gtk::RadioButton _portraitButton;
+ Gtk::RadioButton _landscapeButton;
+ //callbacks
+ void on_portrait();
+ void on_landscape();
+ sigc::connection _portrait_connection;
+ sigc::connection _landscape_connection;
+
+ //### Custom size frame
+ Gtk::Frame _customFrame;
+ Gtk::Grid _customDimTable;
+
+ RegisteredUnitMenu _dimensionUnits;
+ RegisteredScalarUnit _dimensionWidth;
+ RegisteredScalarUnit _dimensionHeight;
+
+ //### Fit Page options
+ Gtk::Expander _fitPageMarginExpander;
+
+ Gtk::Grid _marginTable;
+ Gtk::Box _marginBox;
+ Gtk::Label _marginLabel;
+ RegisteredToggleButton _marginLock;
+ Gtk::Image _lock_icon;
+ RegisteredScalar _marginTop;
+ RegisteredScalar _marginLeft;
+ RegisteredScalar _marginRight;
+ RegisteredScalar _marginBottom;
+ Gtk::Button _fitPageButton;
+ bool _lockMarginUpdate;
+
+ // Document scale
+ Gtk::Frame _scaleFrame;
+ Gtk::Grid _scaleTable;
+
+ Gtk::Label _scaleLabel;
+ RegisteredScalar _scaleX;
+ RegisteredScalar _scaleY;
+ bool _lockScaleUpdate;
+
+ // Viewbox
+ Gtk::Expander _viewboxExpander;
+ Gtk::Grid _viewboxTable;
+
+ RegisteredScalar _viewboxX;
+ RegisteredScalar _viewboxY;
+ RegisteredScalar _viewboxW;
+ RegisteredScalar _viewboxH;
+ Gtk::Box _viewboxSpacer;
+ bool _lockViewboxUpdate;
+
+ //callback
+ void on_value_changed();
+ void on_units_changed();
+ void on_scale_changed();
+ void on_viewbox_changed();
+ sigc::connection _changedw_connection;
+ sigc::connection _changedh_connection;
+ sigc::connection _changedu_connection;
+ sigc::connection _changeds_connection;
+ sigc::connection _changedvx_connection;
+ sigc::connection _changedvy_connection;
+ sigc::connection _changedvw_connection;
+ sigc::connection _changedvh_connection;
+ sigc::connection _changedlk_connection;
+ sigc::connection _changedmt_connection;
+ sigc::connection _changedmb_connection;
+ sigc::connection _changedml_connection;
+ sigc::connection _changedmr_connection;
+
+ Registry *_widgetRegistry;
+
+ //### state - whether we are currently landscape or portrait
+ bool _landscape;
+
+ Glib::ustring _unit;
+
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif // INKSCAPE_UI_WIDGET_PAGE_SIZER_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/widget/pages-skeleton.h b/src/ui/widget/pages-skeleton.h
new file mode 100644
index 0000000..c62e03e
--- /dev/null
+++ b/src/ui/widget/pages-skeleton.h
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * List of paper sizes
+ */
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Bob Jamison <ishmal@users.sf.net>
+ * Abhishek Sharma
+ * + see git history
+ *
+ * Copyright (C) 2000 - 2018 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_PAGES_SKELETON_H
+#define SEEN_PAGES_SKELETON_H
+
+
+ /** \note
+ * The ISO page sizes in the table below differ from ghostscript's idea of page sizes (by
+ * less than 1pt). Being off by <1pt should be OK for most purposes, but may cause fuzziness
+ * (antialiasing) problems when printing to 72dpi or 144dpi printers or bitmap files due to
+ * postscript's different coordinate system (y=0 meaning bottom of page in postscript and top
+ * of page in SVG). I haven't looked into whether this does in fact cause fuzziness, I merely
+ * note the possibility. Rounding done by extension/internal/ps.cpp (e.g. floor/ceil calls)
+ * will also affect whether fuzziness occurs.
+ *
+ * The remainder of this comment discusses the origin of the numbers used for ISO page sizes in
+ * this table and in ghostscript.
+ *
+ * The versions here, in mm, are the official sizes according to
+ * <a href="http://en.wikipedia.org/wiki/Paper_sizes">http://en.wikipedia.org/wiki/Paper_sizes</a>
+ * at 2005-01-25. (The ISO entries in the below table
+ * were produced mechanically from the table on that page.)
+ *
+ * (The rule seems to be that A0, B0, ..., D0. sizes are rounded to the nearest number of mm
+ * from the "theoretical size" (i.e. 1000 * sqrt(2) or pow(2.0, .25) or the like), whereas
+ * going from e.g. A0 to A1 always take the floor of halving -- which by chance coincides
+ * exactly with flooring the "theoretical size" for n != 0 instead of the rounding to nearest
+ * done for n==0.)
+ *
+ * Ghostscript paper sizes are given in gs_statd.ps according to gs(1). gs_statd.ps always
+ * uses an integer number ofpt: sometimes gs_statd.ps rounds to nearest (e.g. a1), sometimes
+ * floors (e.g. a10), sometimes ceils (e.g. a8).
+ *
+ * I'm not sure how ghostscript's gs_statd.ps was calculated: it isn't just rounding the
+ * "theoretical size" of each page topt (see a0), nor is it rounding the a0 size times an
+ * appropriate power of two (see a1). Possibly it was prepared manually, with a human applying
+ * inconsistent rounding rules when converting from mm to pt.
+ */
+ /** \todo
+ * Should we include the JIS B series (used in Japan)
+ * (JIS B0 is sometimes called JB0, and similarly for JB1 etc)?
+ * Should we exclude B7--B10 and A7--10 to make the list smaller ?
+ * Should we include any of the ISO C, D and E series (see below) ?
+ */
+
+
+
+ /* See http://www.hbp.com/content/PCR_envelopes.cfm for a much larger list of US envelope
+ sizes. */
+ /* Note that `Folio' (used in QPrinter/KPrinter) is deliberately absent from this list, as it
+ means different sizes to different people: different people may expect the width to be
+ either 8, 8.25 or 8.5 inches, and the height to be either 13 or 13.5 inches, even
+ restricting our interpretation to foolscap folio. If you wish to introduce a folio-like
+ page size to the list, then please consider using a name more specific than just `Folio' or
+ `Foolscap Folio'. */
+
+static char const pages_skeleton[] = R"(#Inkscape page sizes
+#NAME, WIDTH, HEIGHT, UNIT
+A4, 210, 297, mm
+US Letter, 8.5, 11, in
+US Legal, 8.5, 14, in
+US Executive, 7.25, 10.5, in
+A0, 841, 1189, mm
+A1, 594, 841, mm
+A2, 420, 594, mm
+A3, 297, 420, mm
+A5, 148, 210, mm
+A6, 105, 148, mm
+A7, 74, 105, mm
+A8, 52, 74, mm
+A9, 37, 52, mm
+A10, 26, 37, mm
+B0, 1000, 1414, mm
+B1, 707, 1000, mm
+B2, 500, 707, mm
+B3, 353, 500, mm
+B4, 250, 353, mm
+B5, 176, 250, mm
+B6, 125, 176, mm
+B7, 88, 125, mm
+B8, 62, 88, mm
+B9, 44, 62, mm
+B10, 31, 44, mm
+C0, 917, 1297, mm
+C1, 648, 917, mm
+C2, 458, 648, mm
+C3, 324, 458, mm
+C4, 229, 324, mm
+C5, 162, 229, mm
+C6, 114, 162, mm
+C7, 81, 114, mm
+C8, 57, 81, mm
+C9, 40, 57, mm
+C10, 28, 40, mm
+D1, 545, 771, mm
+D2, 385, 545, mm
+D3, 272, 385, mm
+D4, 192, 272, mm
+D5, 136, 192, mm
+D6, 96, 136, mm
+D7, 68, 96, mm
+E3, 400, 560, mm
+E4, 280, 400, mm
+E5, 200, 280, mm
+E6, 140, 200, mm
+CSE, 462, 649, pt
+US #10 Envelope, 9.5, 4.125, in
+DL Envelope, 220, 110, mm
+Ledger/Tabloid, 11, 17, in
+Banner 468x60, 468, 60, px
+Icon 16x16, 16, 16, px
+Icon 32x32, 32, 32, px
+Icon 48x48, 48, 48, px
+ID Card (ISO 7810), 85.60, 53.98, mm
+Business Card (US), 3.5, 2, in
+Business Card (Europe), 85, 55, mm
+Business Card (Aus/NZ), 90, 55, mm
+Arch A, 9, 12, in
+Arch B, 12, 18, in
+Arch C, 18, 24, in
+Arch D, 24, 36, in
+Arch E, 36, 48, in
+Arch E1, 30, 42, in
+Video SD / PAL, 768, 576, px
+Video SD-Widescreen / PAL, 1024, 576, px
+Video SD / NTSC, 544, 480, px
+Video SD-Widescreen / NTSC, 872, 486, px
+Video HD 720p, 1280, 720, px
+Video HD 1080p, 1920, 1080, px
+Video DCI 2k (Full Frame), 2048, 1080, px
+Video UHD 4k, 3840, 2160, px
+Video DCI 4k (Full Frame), 4096, 2160, px
+Video UHD 8k, 7680, 4320, px
+)";
+
+#endif
diff --git a/src/ui/widget/panel.cpp b/src/ui/widget/panel.cpp
new file mode 100644
index 0000000..8ca08a0
--- /dev/null
+++ b/src/ui/widget/panel.cpp
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2007 Gustav Broberg
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/dialog.h> // for Gtk::RESPONSE_*
+
+#include <glibmm/i18n.h>
+
+#include "panel.h"
+#include "desktop.h"
+
+#include "inkscape.h"
+#include "preview.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void Panel::prep() {
+ GtkIconSize sizes[] = {
+ GTK_ICON_SIZE_MENU,
+ GTK_ICON_SIZE_MENU,
+ GTK_ICON_SIZE_SMALL_TOOLBAR,
+ GTK_ICON_SIZE_BUTTON,
+ GTK_ICON_SIZE_DND, // Not used by options, but included to make the last size larger
+ GTK_ICON_SIZE_DIALOG
+ };
+ Preview::set_size_mappings( G_N_ELEMENTS(sizes), sizes );
+}
+
+Panel::Panel(gchar const *prefs_path, int verb_num) :
+ _prefs_path(prefs_path),
+ _desktop(SP_ACTIVE_DESKTOP),
+ _verb_num(verb_num),
+ _action_area(nullptr)
+{
+ set_name("InkscapePanel");
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ signalResponse().connect(sigc::mem_fun(*this, &Panel::_handleResponse));
+ signalActivateDesktop().connect(sigc::mem_fun(*this, &Panel::setDesktop));
+
+ pack_start(_contents, true, true);
+
+ show_all_children();
+}
+
+Panel::~Panel()
+= default;
+
+void Panel::present()
+{
+ _signal_present.emit();
+}
+
+sigc::signal<void, int> &Panel::signalResponse()
+{
+ return _signal_response;
+}
+
+sigc::signal<void> &Panel::signalPresent()
+{
+ return _signal_present;
+}
+
+gchar const *Panel::getPrefsPath() const
+{
+ return _prefs_path.data();
+}
+
+int const &Panel::getVerb() const
+{
+ return _verb_num;
+}
+
+void Panel::setDesktop(SPDesktop *desktop)
+{
+ _desktop = desktop;
+}
+
+void Panel::_apply()
+{
+ g_warning("Apply button clicked for panel [Panel::_apply()]");
+}
+
+Gtk::Button *Panel::addResponseButton(const Glib::ustring &button_text, int response_id, bool pack_start)
+{
+ // Create a button box for the response buttons if it's the first button to be added
+ if (!_action_area) {
+ _action_area = new Gtk::ButtonBox();
+ _action_area->set_layout(Gtk::BUTTONBOX_END);
+ _action_area->set_spacing(6);
+ _action_area->set_border_width(4);
+ pack_end(*_action_area, Gtk::PACK_SHRINK, 0);
+ }
+
+ Gtk::Button *button = new Gtk::Button(button_text, true);
+
+ _action_area->pack_end(*button);
+
+ if (pack_start) {
+ _action_area->set_child_secondary(*button , true);
+ }
+
+ if (response_id != 0) {
+ // Re-emit clicked signals as response signals
+ button->signal_clicked().connect(sigc::bind(_signal_response.make_slot(), response_id));
+ _response_map[response_id] = button;
+ }
+
+ return button;
+}
+
+void Panel::setResponseSensitive(int response_id, bool setting)
+{
+ if (_response_map[response_id])
+ _response_map[response_id]->set_sensitive(setting);
+}
+
+sigc::signal<void, SPDesktop *, SPDocument *> &
+Panel::signalDocumentReplaced()
+{
+ return _signal_document_replaced;
+}
+
+sigc::signal<void, SPDesktop *> &
+Panel::signalActivateDesktop()
+{
+ return _signal_activate_desktop;
+}
+
+sigc::signal<void, SPDesktop *> &
+Panel::signalDeactiveDesktop()
+{
+ return _signal_deactive_desktop;
+}
+
+void Panel::_handleResponse(int response_id)
+{
+ switch (response_id) {
+ case Gtk::RESPONSE_APPLY: {
+ _apply();
+ break;
+ }
+ }
+}
+
+Inkscape::Selection *Panel::_getSelection()
+{
+ return _desktop->getSelection();
+}
+
+} // namespace Widget
+} // 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/widget/panel.h b/src/ui/widget/panel.h
new file mode 100644
index 0000000..3f08218
--- /dev/null
+++ b/src/ui/widget/panel.h
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2012 Kris De Gussem
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_UI_WIDGET_PANEL_H
+#define SEEN_INKSCAPE_UI_WIDGET_PANEL_H
+
+#include <gtkmm/box.h>
+#include <map>
+
+class SPDesktop;
+class SPDocument;
+
+namespace Gtk {
+ class Button;
+ class ButtonBox;
+}
+
+struct InkscapeApplication;
+
+namespace Inkscape {
+
+class Selection;
+
+namespace UI {
+
+namespace Widget {
+
+/**
+ * A generic dockable container.
+ *
+ * Inkscape::UI::Widget::Panel is a base class from which dockable dialogs
+ * are created. A new dockable dialog is created by deriving a class from panel.
+ * Child widgets are private data members of Panel (no need to use pointers and
+ * new).
+ *
+ * @see UI::Dialog::DesktopTracker to handle desktop change, selection change and selected object modifications.
+ * @see UI::Dialog::DialogManager manages the dialogs within inkscape.
+ */
+class Panel : public Gtk::Box {
+public:
+ static void prep();
+
+ /**
+ * Construct a Panel.
+ *
+ * @param prefs_path characteristic path to load/save dialog position.
+ * @param verb_num the dialog verb.
+ */
+ Panel(gchar const *prefs_path = nullptr, int verb_num = 0);
+ ~Panel() override;
+
+ gchar const *getPrefsPath() const;
+
+ int const &getVerb() const;
+
+ virtual void present(); //< request to be present
+
+ void restorePanelPrefs();
+
+ virtual void setDesktop(SPDesktop *desktop);
+ SPDesktop *getDesktop() { return _desktop; }
+
+ /* Signal accessors */
+ virtual sigc::signal<void, int> &signalResponse();
+ virtual sigc::signal<void> &signalPresent();
+
+ /* Methods providing a Gtk::Dialog like interface for adding buttons that emit Gtk::RESPONSE
+ * signals on click. */
+ Gtk::Button* addResponseButton (const Glib::ustring &button_text, int response_id, bool pack_start=false);
+ void setResponseSensitive(int response_id, bool setting);
+
+ /* Return signals. Signals emitted by PanelDialog. */
+ virtual sigc::signal<void, SPDesktop *, SPDocument *> &signalDocumentReplaced();
+ virtual sigc::signal<void, SPDesktop *> &signalActivateDesktop();
+ virtual sigc::signal<void, SPDesktop *> &signalDeactiveDesktop();
+
+protected:
+ /**
+ * Returns a pointer to a Gtk::Box containing the child widgets.
+ */
+ Gtk::Box *_getContents() { return &_contents; }
+ virtual void _apply();
+
+ virtual void _handleResponse(int response_id);
+
+ /* Helper methods */
+ Inkscape::Selection *_getSelection();
+
+ /**
+ * Stores characteristic path for loading/saving the dialog position.
+ */
+ Glib::ustring const _prefs_path;
+
+ /* Signals */
+ sigc::signal<void, int> _signal_response;
+ sigc::signal<void> _signal_present;
+ sigc::signal<void, SPDesktop *, SPDocument *> _signal_document_replaced;
+ sigc::signal<void, SPDesktop *> _signal_activate_desktop;
+ sigc::signal<void, SPDesktop *> _signal_deactive_desktop;
+
+private:
+ SPDesktop *_desktop;
+
+ int _verb_num;
+
+ Gtk::VBox _contents;
+ Gtk::ButtonBox *_action_area; //< stores response buttons
+
+ /* A map to store which widget that emits a certain response signal */
+ typedef std::map<int, Gtk::Widget *> ResponseMap;
+ ResponseMap _response_map;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_INKSCAPE_UI_WIDGET_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/widget/point.cpp b/src/ui/widget/point.cpp
new file mode 100644
index 0000000..e0d6eed
--- /dev/null
+++ b/src/ui/widget/point.cpp
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2007 Authors
+ * Copyright (C) 2004 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/point.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic),
+ xwidget("X:",""),
+ ywidget("Y:","")
+{
+ static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->show_all_children();
+}
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic),
+ xwidget("X:","", digits),
+ ywidget("Y:","", digits)
+{
+ static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->show_all_children();
+}
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic),
+ xwidget("X:","", adjust, digits),
+ ywidget("Y:","", adjust, digits)
+{
+ static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::VBox*>(_widget)->show_all_children();
+}
+
+unsigned Point::getDigits() const
+{
+ return xwidget.getDigits();
+}
+
+double Point::getStep() const
+{
+ return xwidget.getStep();
+}
+
+double Point::getPage() const
+{
+ return xwidget.getPage();
+}
+
+double Point::getRangeMin() const
+{
+ return xwidget.getRangeMin();
+}
+
+double Point::getRangeMax() const
+{
+ return xwidget.getRangeMax();
+}
+
+double Point::getXValue() const
+{
+ return xwidget.getValue();
+}
+
+double Point::getYValue() const
+{
+ return ywidget.getValue();
+}
+
+Geom::Point Point::getValue() const
+{
+ return Geom::Point( getXValue() , getYValue() );
+}
+
+int Point::getXValueAsInt() const
+{
+ return xwidget.getValueAsInt();
+}
+
+int Point::getYValueAsInt() const
+{
+ return ywidget.getValueAsInt();
+}
+
+
+void Point::setDigits(unsigned digits)
+{
+ xwidget.setDigits(digits);
+ ywidget.setDigits(digits);
+}
+
+void Point::setIncrements(double step, double page)
+{
+ xwidget.setIncrements(step, page);
+ ywidget.setIncrements(step, page);
+}
+
+void Point::setRange(double min, double max)
+{
+ xwidget.setRange(min, max);
+ ywidget.setRange(min, max);
+}
+
+void Point::setValue(Geom::Point const & p)
+{
+ xwidget.setValue(p[0]);
+ ywidget.setValue(p[1]);
+}
+
+void Point::update()
+{
+ xwidget.update();
+ ywidget.update();
+}
+
+bool Point::setProgrammatically()
+{
+ return (xwidget.setProgrammatically || ywidget.setProgrammatically);
+}
+
+void Point::clearProgrammatically()
+{
+ xwidget.setProgrammatically = false;
+ ywidget.setProgrammatically = false;
+}
+
+
+Glib::SignalProxy0<void> Point::signal_x_value_changed()
+{
+ return xwidget.signal_value_changed();
+}
+
+Glib::SignalProxy0<void> Point::signal_y_value_changed()
+{
+ return ywidget.signal_value_changed();
+}
+
+
+} // namespace Widget
+} // 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/widget/point.h b/src/ui/widget/point.h
new file mode 100644
index 0000000..018be5b
--- /dev/null
+++ b/src/ui/widget/point.h
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2007 Authors
+ * Copyright (C) 2004 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_POINT_H
+#define INKSCAPE_UI_WIDGET_POINT_H
+
+#include "ui/widget/labelled.h"
+#include <2geom/point.h>
+#include "ui/widget/scalar.h"
+
+namespace Gtk {
+class Adjustment;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional icon or suffix, for
+ * entering arbitrary coordinate values.
+ */
+class Point : public Labelled
+{
+public:
+
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Fetches the precision of the spin button.
+ */
+ unsigned getDigits() const;
+
+ /**
+ * Gets the current step increment used by the spin button.
+ */
+ double getStep() const;
+
+ /**
+ * Gets the current page increment used by the spin button.
+ */
+ double getPage() const;
+
+ /**
+ * Gets the minimum range value allowed for the spin button.
+ */
+ double getRangeMin() const;
+
+ /**
+ * Gets the maximum range value allowed for the spin button.
+ */
+ double getRangeMax() const;
+
+ bool getSnapToTicks() const;
+
+ /**
+ * Get the value in the spin_button.
+ */
+ double getXValue() const;
+
+ double getYValue() const;
+
+ Geom::Point getValue() const;
+
+ /**
+ * Get the value spin_button represented as an integer.
+ */
+ int getXValueAsInt() const;
+
+ int getYValueAsInt() const;
+
+ /**
+ * Sets the precision to be displayed by the spin button.
+ */
+ void setDigits(unsigned digits);
+
+ /**
+ * Sets the step and page increments for the spin button.
+ */
+ void setIncrements(double step, double page);
+
+ /**
+ * Sets the minimum and maximum range allowed for the spin button.
+ */
+ void setRange(double min, double max);
+
+ /**
+ * Sets the value of the spin button.
+ */
+ void setValue(Geom::Point const & p);
+
+ /**
+ * Manually forces an update of the spin button.
+ */
+ void update();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_x_value_changed();
+
+ Glib::SignalProxy0<void> signal_y_value_changed();
+
+ /**
+ * Check 'setProgrammatically' of both scalar widgets. False if value is changed by user by clicking the widget.
+ * true if the value was set by setValue, not changed by the user;
+ * if a callback checks it, it must reset it back to false.
+ */
+ bool setProgrammatically();
+
+ void clearProgrammatically();
+
+protected:
+ Scalar xwidget;
+ Scalar ywidget;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_POINT_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/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp
new file mode 100644
index 0000000..92c101b
--- /dev/null
+++ b/src/ui/widget/preferences-widget.cpp
@@ -0,0 +1,1023 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape Preferences dialog.
+ *
+ * Authors:
+ * Marco Scholten
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004, 2006, 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/convert.h>
+#include <glibmm/regex.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/table.h>
+
+
+#include "desktop.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "preferences.h"
+#include "selcue.h"
+#include "selection-chemistry.h"
+#include "verbs.h"
+
+#include "include/gtkmm_version.h"
+
+#include "io/sys.h"
+
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/preferences-widget.h"
+
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+using namespace Inkscape::UI::Widget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+DialogPage::DialogPage()
+{
+ set_border_width(12);
+
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+ set_column_spacing(12);
+ set_row_spacing(6);
+}
+
+/**
+ * Add a widget to the bottom row of the dialog page
+ *
+ * \param[in] indent Whether the widget should be indented by one column
+ * \param[in] label The label text for the widget
+ * \param[in] widget The widget to add to the page
+ * \param[in] suffix Text for an optional label at the right of the widget
+ * \param[in] tip Tooltip text for the widget
+ * \param[in] expand_widget Whether to expand the widget horizontally
+ * \param[in] other_widget An optional additional widget to display at the right of the first one
+ */
+void DialogPage::add_line(bool indent,
+ Glib::ustring const &label,
+ Gtk::Widget &widget,
+ Glib::ustring const &suffix,
+ const Glib::ustring &tip,
+ bool expand_widget,
+ Gtk::Widget *other_widget)
+{
+ if (tip != "")
+ widget.set_tooltip_text (tip);
+
+ auto hb = Gtk::manage(new Gtk::Box());
+ hb->set_spacing(12);
+ hb->set_hexpand(true);
+ hb->pack_start(widget, expand_widget, expand_widget);
+
+ // Pack an additional widget into a box with the widget if desired
+ if (other_widget)
+ hb->pack_start(*other_widget, expand_widget, expand_widget);
+
+ hb->set_valign(Gtk::ALIGN_CENTER);
+
+ // Add a label in the first column if provided
+ if (label != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START,
+ Gtk::ALIGN_CENTER, true));
+ label_widget->set_mnemonic_widget(widget);
+ label_widget->set_markup(label_widget->get_text());
+
+ if (indent) {
+ label_widget->set_margin_start(12);
+ }
+
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ add(*label_widget);
+ attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1);
+ }
+
+ // Now add the widget to the bottom of the dialog
+ if (label == "")
+ {
+ if (indent) {
+ hb->set_margin_start(12);
+ }
+
+ add(*hb);
+
+ GValue width = G_VALUE_INIT;
+ g_value_init(&width, G_TYPE_INT);
+ g_value_set_int(&width, 2);
+ gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(hb->gobj()), "width", &width);
+ }
+
+ // Add a label on the right of the widget if desired
+ if (suffix != "")
+ {
+ Gtk::Label* suffix_widget = Gtk::manage(new Gtk::Label(suffix , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+ suffix_widget->set_markup(suffix_widget->get_text());
+ hb->pack_start(*suffix_widget,false,false);
+ }
+
+}
+
+void DialogPage::add_group_header(Glib::ustring name)
+{
+ if (name != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring(/*"<span size='large'>*/"<b>") + name +
+ Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+
+ label_widget->set_use_markup(true);
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ add(*label_widget);
+ }
+}
+
+void DialogPage::set_tip(Gtk::Widget& widget, Glib::ustring const &tip)
+{
+ widget.set_tooltip_text (tip);
+}
+
+void PrefCheckButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ bool default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->set_label(label);
+ this->set_active( prefs->getBool(_prefs_path, default_value) );
+}
+
+void PrefCheckButton::on_toggled()
+{
+ this->changed_signal.emit(this->get_active());
+ if (this->get_visible()) //only take action if the user toggled it
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(_prefs_path, this->get_active());
+ }
+}
+
+void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member)
+{
+ _prefs_path = prefs_path;
+ _value_type = VAL_STRING;
+ _string_value = string_value;
+ (void)default_value;
+ this->set_label(label);
+ if (group_member)
+ {
+ Gtk::RadioButtonGroup rbg = group_member->get_group();
+ this->set_group(rbg);
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring val = prefs->getString(_prefs_path);
+ if ( !val.empty() )
+ this->set_active(val == _string_value);
+ else
+ this->set_active( false );
+}
+
+void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ int int_value, bool default_value, PrefRadioButton* group_member)
+{
+ _prefs_path = prefs_path;
+ _value_type = VAL_INT;
+ _int_value = int_value;
+ this->set_label(label);
+ if (group_member)
+ {
+ Gtk::RadioButtonGroup rbg = group_member->get_group();
+ this->set_group(rbg);
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (default_value)
+ this->set_active( prefs->getInt(_prefs_path, int_value) == _int_value );
+ else
+ this->set_active( prefs->getInt(_prefs_path, int_value + 1) == _int_value );
+}
+
+void PrefRadioButton::on_toggled()
+{
+ this->changed_signal.emit(this->get_active());
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (this->get_visible() && this->get_active() ) //only take action if toggled by user (to active)
+ {
+ if ( _value_type == VAL_STRING )
+ prefs->setString(_prefs_path, _string_value);
+ else if ( _value_type == VAL_INT )
+ prefs->setInt(_prefs_path, _int_value);
+ }
+}
+
+void PrefSpinButton::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double /*page_increment*/,
+ double default_value, bool is_int, bool is_percent)
+{
+ _prefs_path = prefs_path;
+ _is_int = is_int;
+ _is_percent = is_percent;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value;
+ if (is_int) {
+ if (is_percent) {
+ value = 100 * prefs->getDoubleLimited(prefs_path, default_value, lower/100.0, upper/100.0);
+ } else {
+ value = (double) prefs->getIntLimited(prefs_path, (int) default_value, (int) lower, (int) upper);
+ }
+ } else {
+ value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+ }
+
+ this->set_range (lower, upper);
+ this->set_increments (step_increment, 0);
+ this->set_value (value);
+ this->set_width_chars(6);
+ if (is_int)
+ this->set_digits(0);
+ else if (step_increment < 0.1)
+ this->set_digits(4);
+ else
+ this->set_digits(2);
+
+}
+
+void PrefSpinButton::on_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (this->get_visible()) //only take action if user changed value
+ {
+ if (_is_int) {
+ if (_is_percent) {
+ prefs->setDouble(_prefs_path, this->get_value()/100.0);
+ } else {
+ prefs->setInt(_prefs_path, (int) this->get_value());
+ }
+ } else {
+ prefs->setDouble(_prefs_path, this->get_value());
+ }
+ }
+}
+
+void PrefSpinUnit::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment,
+ double default_value, UnitType unit_type, Glib::ustring const &default_unit)
+{
+ _prefs_path = prefs_path;
+ _is_percent = (unit_type == UNIT_TYPE_DIMENSIONLESS);
+
+ resetUnitType(unit_type);
+ setUnit(default_unit);
+ setRange (lower, upper); /// @fixme this disregards changes of units
+ setIncrements (step_increment, 0);
+ if (step_increment < 0.1) {
+ setDigits(4);
+ } else {
+ setDigits(2);
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+ Glib::ustring unitstr = prefs->getUnit(prefs_path);
+ if (unitstr.length() == 0) {
+ unitstr = default_unit;
+ // write the assumed unit to preferences:
+ prefs->setDoubleUnit(_prefs_path, value, unitstr);
+ }
+ setValue(value, unitstr);
+
+ signal_value_changed().connect_notify(sigc::mem_fun(*this, &PrefSpinUnit::on_my_value_changed));
+}
+
+void PrefSpinUnit::on_my_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (getWidget()->get_visible()) //only take action if user changed value
+ {
+ prefs->setDoubleUnit(_prefs_path, getValue(getUnit()->abbr), getUnit()->abbr);
+ }
+}
+
+const double ZoomCorrRuler::textsize = 7;
+const double ZoomCorrRuler::textpadding = 5;
+
+ZoomCorrRuler::ZoomCorrRuler(int width, int height) :
+ _unitconv(1.0),
+ _border(5)
+{
+ set_size(width, height);
+}
+
+void ZoomCorrRuler::set_size(int x, int y)
+{
+ _min_width = x;
+ _height = y;
+ set_size_request(x + _border*2, y + _border*2);
+}
+
+// The following two functions are borrowed from 2geom's toy-framework-2; if they are useful in
+// other locations, we should perhaps make them (or adapted versions of them) publicly available
+static void
+draw_text(cairo_t *cr, Geom::Point loc, const char* txt, bool bottom = false,
+ double fontsize = ZoomCorrRuler::textsize, std::string fontdesc = "Sans") {
+ PangoLayout* layout = pango_cairo_create_layout (cr);
+ pango_layout_set_text(layout, txt, -1);
+
+ // set font and size
+ std::ostringstream sizestr;
+ sizestr << fontsize;
+ fontdesc = fontdesc + " " + sizestr.str();
+ PangoFontDescription *font_desc = pango_font_description_from_string(fontdesc.c_str());
+ pango_layout_set_font_description(layout, font_desc);
+ pango_font_description_free (font_desc);
+
+ PangoRectangle logical_extent;
+ pango_layout_get_pixel_extents(layout, nullptr, &logical_extent);
+ cairo_move_to(cr, loc[Geom::X], loc[Geom::Y] - (bottom ? logical_extent.height : 0));
+ pango_cairo_show_layout(cr, layout);
+}
+
+static void
+draw_number(cairo_t *cr, Geom::Point pos, double num) {
+ std::ostringstream number;
+ number << num;
+ draw_text(cr, pos, number.str().c_str(), true);
+}
+
+/*
+ * \arg dist The distance between consecutive minor marks
+ * \arg major_interval Number of marks after which to draw a major mark
+ */
+void
+ZoomCorrRuler::draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ const double zoomcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0);
+ double mark = 0;
+ int i = 0;
+ while (mark <= _drawing_width) {
+ cr->move_to(mark, _height);
+ if ((i % major_interval) == 0) {
+ // major mark
+ cr->line_to(mark, 0);
+ Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding);
+ draw_number(cr->cobj(), textpos, dist * i);
+ } else {
+ // minor mark
+ cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding);
+ }
+ mark += dist * zoomcorr / _unitconv;
+ ++i;
+ }
+}
+
+bool
+ZoomCorrRuler::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ Glib::RefPtr<Gdk::Window> window = get_window();
+
+ int w = window->get_width();
+ _drawing_width = w - _border * 2;
+
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->set_fill_rule(Cairo::FILL_RULE_WINDING);
+ cr->rectangle(0, 0, w, _height + _border*2);
+ cr->fill();
+
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(0.5);
+
+ cr->translate(_border, _border); // so that we have a small white border around the ruler
+ cr->move_to (0, _height);
+ cr->line_to (_drawing_width, _height);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring abbr = prefs->getString("/options/zoomcorrection/unit");
+ if (abbr == "cm") {
+ draw_marks(cr, 0.1, 10);
+ } else if (abbr == "in") {
+ draw_marks(cr, 0.25, 4);
+ } else if (abbr == "mm") {
+ draw_marks(cr, 10, 10);
+ } else if (abbr == "pc") {
+ draw_marks(cr, 1, 10);
+ } else if (abbr == "pt") {
+ draw_marks(cr, 10, 10);
+ } else if (abbr == "px") {
+ draw_marks(cr, 10, 10);
+ } else {
+ draw_marks(cr, 1, 1);
+ }
+ cr->stroke();
+
+ return true;
+}
+
+
+void
+ZoomCorrRulerSlider::on_slider_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/options/zoomcorrection/value", _slider->get_value() / 100.0);
+ _sb.set_value(_slider->get_value());
+ _ruler.queue_draw();
+ freeze = false;
+ }
+}
+
+void
+ZoomCorrRulerSlider::on_spinbutton_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/options/zoomcorrection/value", _sb.get_value() / 100.0);
+ _slider->set_value(_sb.get_value());
+ _ruler.queue_draw();
+ freeze = false;
+ }
+}
+
+void
+ZoomCorrRulerSlider::on_unit_changed() {
+ if (GPOINTER_TO_INT(_unit.get_data("sensitive")) == 0) {
+ // when the unit menu is initialized, the unit is set to the default but
+ // it needs to be reset later so we don't perform the change in this case
+ return;
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/zoomcorrection/unit", _unit.getUnitAbbr());
+ double conv = _unit.getConversion(_unit.getUnitAbbr(), "px");
+ _ruler.set_unit_conversion(conv);
+ if (_ruler.get_visible()) {
+ _ruler.queue_draw();
+ }
+}
+
+bool ZoomCorrRulerSlider::on_mnemonic_activate ( bool group_cycling )
+{
+ return _sb.mnemonic_activate ( group_cycling );
+}
+
+
+void
+ZoomCorrRulerSlider::init(int ruler_width, int ruler_height, double lower, double upper,
+ double step_increment, double page_increment, double default_value)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/zoomcorrection/value", default_value, lower, upper) * 100.0;
+
+ freeze = false;
+
+ _ruler.set_size(ruler_width, ruler_height);
+
+ _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ _slider->set_size_request(_ruler.width(), -1);
+ _slider->set_range (lower, upper);
+ _slider->set_increments (step_increment, page_increment);
+ _slider->set_value (value);
+ _slider->set_digits(2);
+
+ _slider->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_slider_value_changed));
+ _sb.signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_spinbutton_value_changed));
+ _unit.signal_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_unit_changed));
+
+ _sb.set_range (lower, upper);
+ _sb.set_increments (step_increment, 0);
+ _sb.set_value (value);
+ _sb.set_digits(2);
+ _sb.set_halign(Gtk::ALIGN_CENTER);
+ _sb.set_valign(Gtk::ALIGN_END);
+
+ _unit.set_data("sensitive", GINT_TO_POINTER(0));
+ _unit.setUnitType(UNIT_TYPE_LINEAR);
+ _unit.set_data("sensitive", GINT_TO_POINTER(1));
+ _unit.setUnit(prefs->getString("/options/zoomcorrection/unit"));
+ _unit.set_halign(Gtk::ALIGN_CENTER);
+ _unit.set_valign(Gtk::ALIGN_END);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->attach(*_slider, 0, 0, 1, 1);
+ table->attach(_sb, 1, 0, 1, 1);
+ table->attach(_ruler, 0, 1, 1, 1);
+ table->attach(_unit, 1, 1, 1, 1);
+
+ pack_start(*table, Gtk::PACK_SHRINK);
+}
+
+void
+PrefSlider::on_slider_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(_prefs_path, _slider->get_value());
+ _sb.set_value(_slider->get_value());
+ freeze = false;
+ }
+}
+
+void
+PrefSlider::on_spinbutton_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(_prefs_path, _sb.get_value());
+ _slider->set_value(_sb.get_value());
+ freeze = false;
+ }
+}
+
+bool PrefSlider::on_mnemonic_activate ( bool group_cycling )
+{
+ return _sb.mnemonic_activate ( group_cycling );
+}
+
+void
+PrefSlider::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment, double default_value, int digits)
+{
+ _prefs_path = prefs_path;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+
+ freeze = false;
+
+ _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ _slider->set_range (lower, upper);
+ _slider->set_increments (step_increment, page_increment);
+ _slider->set_value (value);
+ _slider->set_digits(digits);
+ _slider->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_slider_value_changed));
+
+ _sb.signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_spinbutton_value_changed));
+ _sb.set_range (lower, upper);
+ _sb.set_increments (step_increment, 0);
+ _sb.set_value (value);
+ _sb.set_digits(digits);
+ _sb.set_halign(Gtk::ALIGN_CENTER);
+ _sb.set_valign(Gtk::ALIGN_END);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ _slider->set_hexpand();
+ table->attach(*_slider, 0, 0, 1, 1);
+ table->attach(_sb, 1, 0, 1, 1);
+
+ this->pack_start(*table, Gtk::PACK_EXPAND_WIDGET);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], int values[], int num_items, int default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if(value.empty())
+ {
+ value = default_value;
+ }
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels,
+ std::vector<Glib::ustring> values, Glib::ustring default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if (value.empty()) {
+ value = default_value;
+ }
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if(!_values.empty())
+ {
+ prefs->setInt(_prefs_path, _values[this->get_active_row_number()]);
+ }
+ else
+ {
+ prefs->setString(_prefs_path, _ustr_values[this->get_active_row_number()]);
+ }
+ }
+}
+
+void PrefEntryButtonHBox::init(Glib::ustring const &prefs_path,
+ bool visibility, Glib::ustring const &default_string)
+{
+ _prefs_path = prefs_path;
+ _default_string = default_string;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ relatedEntry = new Gtk::Entry();
+ relatedButton = new Gtk::Button(_("Reset"));
+ relatedEntry->set_invisible_char('*');
+ relatedEntry->set_visibility(visibility);
+ relatedEntry->set_text(prefs->getString(_prefs_path));
+ this->pack_start(*relatedEntry);
+ this->pack_start(*relatedButton);
+ relatedButton->signal_clicked().connect(
+ sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedButtonClickedCallback));
+ relatedEntry->signal_changed().connect(
+ sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedEntryChangedCallback));
+}
+
+void PrefEntryButtonHBox::onRelatedEntryChangedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, relatedEntry->get_text());
+ }
+}
+
+void PrefEntryButtonHBox::onRelatedButtonClickedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, _default_string);
+ relatedEntry->set_text(_default_string);
+ }
+}
+
+bool PrefEntryButtonHBox::on_mnemonic_activate ( bool group_cycling )
+{
+ return relatedEntry->mnemonic_activate ( group_cycling );
+}
+
+void PrefEntryFileButtonHBox::init(Glib::ustring const &prefs_path,
+ bool visibility)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ relatedEntry = new Gtk::Entry();
+ relatedEntry->set_invisible_char('*');
+ relatedEntry->set_visibility(visibility);
+ relatedEntry->set_text(prefs->getString(_prefs_path));
+
+ relatedButton = new Gtk::Button();
+ Gtk::HBox* pixlabel = new Gtk::HBox(false, 3);
+ Gtk::Image *im = sp_get_icon_image("applications-graphics", Gtk::ICON_SIZE_BUTTON);
+ pixlabel->pack_start(*im);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup_with_mnemonic(_("_Browse..."));
+ pixlabel->pack_start(*l);
+ relatedButton->add(*pixlabel);
+
+ this->pack_end(*relatedButton, false, false, 4);
+ this->pack_start(*relatedEntry, true, true, 0);
+
+ relatedButton->signal_clicked().connect(
+ sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedButtonClickedCallback));
+ relatedEntry->signal_changed().connect(
+ sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedEntryChangedCallback));
+}
+
+void PrefEntryFileButtonHBox::onRelatedEntryChangedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, relatedEntry->get_text());
+ }
+}
+
+static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr;
+
+void PrefEntryFileButtonHBox::onRelatedButtonClickedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ //# 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 = "";
+
+#ifdef _WIN32
+ //# If no open path, default to our win32 documents folder
+ if (open_path.empty())
+ {
+ // The path to the My Documents folder is read from the
+ // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal"
+ HKEY key = NULL;
+ if(RegOpenKeyExA(HKEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders",
+ 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
+ {
+ WCHAR utf16path[_MAX_PATH];
+ DWORD value_type;
+ DWORD data_size = sizeof(utf16path);
+ if(RegQueryValueExW(key, L"Personal", NULL, &value_type,
+ (BYTE*)utf16path, &data_size) == ERROR_SUCCESS)
+ {
+ g_assert(value_type == REG_SZ);
+ gchar *utf8path = g_utf16_to_utf8(
+ (const gunichar2*)utf16path, -1, NULL, NULL, NULL);
+ if(utf8path)
+ {
+ open_path = Glib::ustring(utf8path);
+ g_free(utf8path);
+ }
+ }
+ }
+ }
+#endif
+
+ //# 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::EXE_TYPES,
+ _("Select a bitmap editor"));
+ }
+
+ //# Show the dialog
+ bool const success = selectPrefsFileInstance->show();
+
+ if (!success) {
+ return;
+ }
+
+ //# User selected something. Get name and type
+ Glib::ustring fileName = selectPrefsFileInstance->getFilename();
+
+ if (!fileName.empty())
+ {
+ Glib::ustring newFileName = Glib::filename_to_utf8(fileName);
+
+ if ( newFileName.size() > 0)
+ open_path = newFileName;
+ else
+ g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" );
+
+ prefs->setString(_prefs_path, open_path);
+ }
+
+ relatedEntry->set_text(fileName);
+ }
+}
+
+bool PrefEntryFileButtonHBox::on_mnemonic_activate ( bool group_cycling )
+{
+ return relatedEntry->mnemonic_activate ( group_cycling );
+}
+
+void PrefOpenFolder::init(Glib::ustring const &entry_string, Glib::ustring const &tooltip)
+{
+ relatedEntry = new Gtk::Entry();
+ relatedButton = new Gtk::Button();
+ Gtk::HBox *pixlabel = new Gtk::HBox(false, 3);
+ Gtk::Image *im = sp_get_icon_image("document-open", Gtk::ICON_SIZE_BUTTON);
+ pixlabel->pack_start(*im);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup_with_mnemonic(_("Open"));
+ pixlabel->pack_start(*l);
+ relatedButton->add(*pixlabel);
+ relatedButton->set_tooltip_text(tooltip);
+ relatedEntry->set_text(entry_string);
+ relatedEntry->set_sensitive(false);
+ this->pack_end(*relatedButton, false, false, 4);
+ this->pack_start(*relatedEntry, true, true, 0);
+ relatedButton->signal_clicked().connect(sigc::mem_fun(*this, &PrefOpenFolder::onRelatedButtonClickedCallback));
+}
+
+void PrefOpenFolder::onRelatedButtonClickedCallback()
+{
+ g_mkdir_with_parents(relatedEntry->get_text().c_str(), 0700);
+ // https://stackoverflow.com/questions/42442189/how-to-open-spawn-a-file-with-glib-gtkmm-in-windows
+#ifdef _WIN32
+ ShellExecute(NULL, "open", relatedEntry->get_text().c_str(), NULL, NULL, SW_SHOWDEFAULT);
+#elif defined(__APPLE__)
+ std::vector<std::string> argv = { "open", relatedEntry->get_text().raw() };
+ Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
+#else
+ gchar *path = g_filename_to_uri(relatedEntry->get_text().c_str(), NULL, NULL);
+ Glib::ustring xgd = "xdg-open ";
+ xgd += path;
+ system((xgd).c_str());
+ g_free(path);
+#endif
+}
+
+void PrefFileButton::init(Glib::ustring const &prefs_path)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ select_filename(Glib::filename_from_utf8(prefs->getString(_prefs_path)));
+
+ signal_selection_changed().connect(sigc::mem_fun(*this, &PrefFileButton::onFileChanged));
+}
+
+void PrefFileButton::onFileChanged()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, Glib::filename_to_utf8(get_filename()));
+}
+
+void PrefEntry::init(Glib::ustring const &prefs_path, bool visibility)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->set_invisible_char('*');
+ this->set_visibility(visibility);
+ this->set_text(prefs->getString(_prefs_path));
+}
+
+void PrefEntry::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, this->get_text());
+ }
+}
+
+void PrefMultiEntry::init(Glib::ustring const &prefs_path, int height)
+{
+ // TODO: Figure out if there's a way to specify height in lines instead of px
+ // and how to obtain a reasonable default width if 'expand_widget' is not used
+ set_size_request(100, height);
+ set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ set_shadow_type(Gtk::SHADOW_IN);
+
+ add(_text);
+
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = prefs->getString(_prefs_path);
+ value = Glib::Regex::create("\\|")->replace_literal(value, 0, "\n", (Glib::RegexMatchFlags)0);
+ _text.get_buffer()->set_text(value);
+ _text.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &PrefMultiEntry::on_changed));
+}
+
+void PrefMultiEntry::on_changed()
+{
+ if (get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = _text.get_buffer()->get_text();
+ value = Glib::Regex::create("\\n")->replace_literal(value, 0, "|", (Glib::RegexMatchFlags)0);
+ prefs->setString(_prefs_path, value);
+ }
+}
+
+void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ guint32 default_rgba)
+{
+ _prefs_path = prefs_path;
+ _title = label;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) );
+}
+
+void PrefColorPicker::on_changed (guint32 rgba)
+{
+ if (this->get_visible()) //only take action if the user toggled it
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt(_prefs_path, (int) rgba);
+ }
+}
+
+void PrefUnit::init(Glib::ustring const &prefs_path)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ setUnitType(UNIT_TYPE_LINEAR);
+ setUnit(prefs->getString(_prefs_path));
+}
+
+void PrefUnit::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, getUnitAbbr());
+ }
+}
+
+} // namespace Widget
+} // 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/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h
new file mode 100644
index 0000000..3e132c0
--- /dev/null
+++ b/src/ui/widget/preferences-widget.h
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widgets for Inkscape Preferences dialog.
+ */
+/*
+ * Authors:
+ * Marco Scholten
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004, 2006, 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+#define INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+
+#include <iostream>
+#include <vector>
+
+#include <gtkmm/filechooserbutton.h>
+#include "ui/widget/spinbutton.h"
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/grid.h>
+
+#include "ui/widget/color-picker.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/scalar-unit.h"
+
+namespace Gtk {
+class Scale;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PrefCheckButton : public Gtk::CheckButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ bool default_value);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ void on_toggled() override;
+};
+
+class PrefRadioButton : public Gtk::RadioButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ int int_value, bool default_value, PrefRadioButton* group_member);
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ Glib::ustring _string_value;
+ int _value_type;
+ enum
+ {
+ VAL_INT,
+ VAL_STRING
+ };
+ int _int_value;
+ void on_toggled() override;
+};
+
+class PrefSpinButton : public SpinButton
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment,
+ double default_value, bool is_int, bool is_percent);
+protected:
+ Glib::ustring _prefs_path;
+ bool _is_int;
+ bool _is_percent;
+ void on_value_changed() override;
+};
+
+class PrefSpinUnit : public ScalarUnit
+{
+public:
+ PrefSpinUnit() : ScalarUnit("", "") {};
+
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment,
+ double default_value,
+ UnitType unit_type, Glib::ustring const &default_unit);
+protected:
+ Glib::ustring _prefs_path;
+ bool _is_percent;
+ void on_my_value_changed();
+};
+
+class ZoomCorrRuler : public Gtk::DrawingArea {
+public:
+ ZoomCorrRuler(int width = 100, int height = 20);
+ void set_size(int x, int y);
+ void set_unit_conversion(double conv) { _unitconv = conv; }
+
+ int width() { return _min_width + _border*2; }
+
+ static const double textsize;
+ static const double textpadding;
+
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+
+ void draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval);
+
+ double _unitconv;
+ int _min_width;
+ int _height;
+ int _border;
+ int _drawing_width;
+};
+
+class ZoomCorrRulerSlider : public Gtk::VBox
+{
+public:
+ void init(int ruler_width, int ruler_height, double lower, double upper,
+ double step_increment, double page_increment, double default_value);
+
+private:
+ void on_slider_value_changed();
+ void on_spinbutton_value_changed();
+ void on_unit_changed();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+
+ Inkscape::UI::Widget::SpinButton _sb;
+ UnitMenu _unit;
+ Gtk::Scale* _slider;
+ ZoomCorrRuler _ruler;
+ bool freeze; // used to block recursive updates of slider and spinbutton
+};
+
+class PrefSlider : public Gtk::HBox
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment, double default_value, int digits);
+
+private:
+ void on_slider_value_changed();
+ void on_spinbutton_value_changed();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+
+ Glib::ustring _prefs_path;
+ Inkscape::UI::Widget::SpinButton _sb;
+
+ Gtk::Scale* _slider;
+
+ bool freeze; // used to block recursive updates of slider and spinbutton
+};
+
+
+class PrefCombo : public Gtk::ComboBoxText
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], int values[], int num_items, int default_value);
+
+ /**
+ * Initialize a combo box.
+ * second form uses strings as key values.
+ */
+ void init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value);
+ /**
+ * Initialize a combo box.
+ * with vectors.
+ */
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value);
+
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<Glib::ustring> values,
+ Glib::ustring default_value);
+
+ protected:
+ Glib::ustring _prefs_path;
+ std::vector<int> _values;
+ std::vector<Glib::ustring> _ustr_values; ///< string key values used optionally instead of numeric _values
+ void on_changed() override;
+};
+
+class PrefEntry : public Gtk::Entry
+{
+public:
+ void init(Glib::ustring const &prefs_path, bool mask);
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed() override;
+};
+
+class PrefMultiEntry : public Gtk::ScrolledWindow
+{
+public:
+ void init(Glib::ustring const &prefs_path, int height);
+protected:
+ Glib::ustring _prefs_path;
+ Gtk::TextView _text;
+ void on_changed();
+};
+
+class PrefEntryButtonHBox : public Gtk::HBox
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ bool mask, Glib::ustring const &default_string);
+
+protected:
+ Glib::ustring _prefs_path;
+ Glib::ustring _default_string;
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedEntryChangedCallback();
+ void onRelatedButtonClickedCallback();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+};
+
+class PrefEntryFileButtonHBox : public Gtk::HBox
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ bool mask);
+protected:
+ Glib::ustring _prefs_path;
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedEntryChangedCallback();
+ void onRelatedButtonClickedCallback();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+};
+
+class PrefOpenFolder : public Gtk::HBox {
+ public:
+ void init(Glib::ustring const &entry_string, Glib::ustring const &tooltip);
+
+ protected:
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedButtonClickedCallback();
+};
+
+class PrefFileButton : public Gtk::FileChooserButton
+{
+public:
+ void init(Glib::ustring const &prefs_path);
+
+protected:
+ Glib::ustring _prefs_path;
+ void onFileChanged();
+};
+
+class PrefColorPicker : public ColorPicker
+{
+public:
+ PrefColorPicker() : ColorPicker("", "", 0, false) {};
+ ~PrefColorPicker() override = default;;
+
+ void init(Glib::ustring const &abel, Glib::ustring const &prefs_path,
+ guint32 default_rgba);
+
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed (guint32 rgba) override;
+};
+
+class PrefUnit : public UnitMenu
+{
+public:
+ void init(Glib::ustring const &prefs_path);
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed() override;
+};
+
+class DialogPage : public Gtk::Grid
+{
+public:
+ DialogPage();
+ void add_line(bool indent, Glib::ustring const &label, Gtk::Widget& widget, Glib::ustring const &suffix, Glib::ustring const &tip, bool expand = true, Gtk::Widget *other_widget = nullptr);
+ void add_group_header(Glib::ustring name);
+ void set_tip(Gtk::Widget &widget, Glib::ustring const &tip);
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_WIDGET_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/widget/preview.cpp b/src/ui/widget/preview.cpp
new file mode 100644
index 0000000..a56639c
--- /dev/null
+++ b/src/ui/widget/preview.cpp
@@ -0,0 +1,502 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include <algorithm>
+#include <gdkmm/general.h>
+#include "preview.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+#define PRIME_BUTTON_MAGIC_NUMBER 1
+
+/* Keep in sync with last value in eek-preview.h */
+#define PREVIEW_SIZE_LAST PREVIEW_SIZE_HUGE
+#define PREVIEW_SIZE_NEXTFREE (PREVIEW_SIZE_HUGE + 1)
+
+#define PREVIEW_MAX_RATIO 500
+
+void
+Preview::set_color(int r, int g, int b )
+{
+ _r = r;
+ _g = g;
+ _b = b;
+
+ queue_draw();
+}
+
+
+void
+Preview::set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf)
+{
+ _previewPixbuf = pixbuf;
+
+ queue_draw();
+
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = _previewPixbuf->get_width();
+ _scaledH = _previewPixbuf->get_height();
+}
+
+static gboolean setupDone = FALSE;
+static GtkRequisition sizeThings[PREVIEW_SIZE_NEXTFREE];
+
+void
+Preview::set_size_mappings( guint count, GtkIconSize const* sizes )
+{
+ gint width = 0;
+ gint height = 0;
+ gint smallest = 512;
+ gint largest = 0;
+ guint i = 0;
+ guint delta = 0;
+
+ for ( i = 0; i < count; ++i ) {
+ gboolean worked = gtk_icon_size_lookup( sizes[i], &width, &height );
+ if ( worked ) {
+ if ( width < smallest ) {
+ smallest = width;
+ }
+ if ( width > largest ) {
+ largest = width;
+ }
+ }
+ }
+
+ smallest = (smallest * 3) / 4;
+
+ delta = largest - smallest;
+
+ for ( i = 0; i < G_N_ELEMENTS(sizeThings); ++i ) {
+ guint val = smallest + ( (i * delta) / (G_N_ELEMENTS(sizeThings) - 1) );
+ sizeThings[i].width = val;
+ sizeThings[i].height = val;
+ }
+
+ setupDone = TRUE;
+}
+
+void
+Preview::size_request(GtkRequisition* req) const
+{
+ int width = 0;
+ int height = 0;
+
+ if ( !setupDone ) {
+ GtkIconSize sizes[] = {
+ GTK_ICON_SIZE_MENU,
+ GTK_ICON_SIZE_SMALL_TOOLBAR,
+ GTK_ICON_SIZE_LARGE_TOOLBAR,
+ GTK_ICON_SIZE_BUTTON,
+ GTK_ICON_SIZE_DIALOG
+ };
+ set_size_mappings( G_N_ELEMENTS(sizes), sizes );
+ }
+
+ width = sizeThings[_size].width;
+ height = sizeThings[_size].height;
+
+ if ( _view == VIEW_TYPE_LIST ) {
+ width *= 3;
+ }
+
+ if ( _ratio != 100 ) {
+ width = (width * _ratio) / 100;
+ if ( width < 0 ) {
+ width = 1;
+ }
+ }
+
+ req->width = width;
+ req->height = height;
+}
+
+void
+Preview::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void
+Preview::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+bool
+Preview::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ auto allocation = get_allocation();
+
+ gint insetTop = 0, insetBottom = 0;
+ gint insetLeft = 0, insetRight = 0;
+
+ if (_border == BORDER_SOLID) {
+ insetTop = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_SOLID_LAST_ROW) {
+ insetTop = insetBottom = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_WIDE) {
+ insetTop = insetBottom = 1;
+ insetLeft = insetRight = 1;
+ }
+
+ auto context = get_style_context();
+
+ context->render_frame(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ context->render_background(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ cr->set_source_rgb(_r/65535.0, _g/65535.0, _b/65535.0 );
+ cr->rectangle(insetLeft, insetTop, allocation.get_width() - (insetLeft + insetRight), allocation.get_height() - (insetTop + insetBottom));
+ cr->fill();
+
+ if (_previewPixbuf )
+ {
+ if ((allocation.get_width() != _scaledW) || (allocation.get_height() != _scaledH)) {
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = allocation.get_width() - (insetLeft + insetRight);
+ _scaledH = allocation.get_height() - (insetTop + insetBottom);
+
+ _scaled = _previewPixbuf->scale_simple(_scaledW,
+ _scaledH,
+ Gdk::INTERP_BILINEAR);
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> pix = (_scaled) ? _scaled : _previewPixbuf;
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ Gdk::Cairo::set_source_pixbuf(cr, pix, insetLeft, insetTop);
+ cr->paint();
+ }
+
+ if (_linked)
+ {
+ /* Draw arrow */
+ GdkRectangle possible = {insetLeft,
+ insetTop,
+ (allocation.get_width() - (insetLeft + insetRight)),
+ (allocation.get_height() - (insetTop + insetBottom))
+ };
+
+ GdkRectangle area = {possible.x,
+ possible.y,
+ possible.width / 2,
+ possible.height / 2 };
+
+ /* Make it square */
+ if ( area.width > area.height )
+ area.width = area.height;
+ if ( area.height > area.width )
+ area.height = area.width;
+
+ /* Center it horizontally */
+ if ( area.width < possible.width ) {
+ int diff = (possible.width - area.width) / 2;
+ area.x += diff;
+ }
+
+ if (_linked & PREVIEW_LINK_IN)
+ {
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ area.x, area.y,
+ std::min(area.width, area.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OUT)
+ {
+ GdkRectangle otherArea = {area.x, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height);
+ }
+
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OTHER)
+ {
+ GdkRectangle otherArea = {insetLeft, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+
+ context->render_arrow(cr,
+ 1.5*G_PI, // Left-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+
+ if (_linked & PREVIEW_FILL)
+ {
+ GdkRectangle otherArea = {possible.x + ((possible.width / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+
+ if (_linked & PREVIEW_STROKE)
+ {
+ GdkRectangle otherArea = {possible.x + (((possible.width * 3) / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ // This should be a diamond too?
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+ }
+
+
+ if ( has_focus() ) {
+ allocation = get_allocation();
+
+ context->render_focus(cr,
+ 0 + 1, 0 + 1,
+ allocation.get_width() - 2, allocation.get_height() - 2 );
+ }
+
+ return false;
+}
+
+
+bool
+Preview::on_enter_notify_event(GdkEventCrossing* event )
+{
+ _within = true;
+ set_state_flags(_hot ? Gtk::STATE_FLAG_ACTIVE : Gtk::STATE_FLAG_PRELIGHT, false);
+
+ return false;
+}
+
+bool
+Preview::on_leave_notify_event(GdkEventCrossing* event)
+{
+ _within = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ return false;
+}
+
+bool
+Preview::on_button_press_event(GdkEventButton *event)
+{
+ if (_takesFocus && !has_focus() )
+ {
+ grab_focus();
+ }
+
+ if ( event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2 )
+ {
+ _hot = true;
+
+ if ( _within )
+ {
+ set_state_flags(Gtk::STATE_FLAG_ACTIVE, false);
+ }
+ }
+
+ return false;
+}
+
+bool
+Preview::on_button_release_event(GdkEventButton* event)
+{
+ _hot = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ if (_within &&
+ (event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2))
+ {
+ gboolean isAlt = ( ((event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) ||
+ (event->button == 2));
+
+ if ( isAlt )
+ {
+ _signal_alt_clicked(2);
+ }
+ else
+ {
+ _signal_clicked.emit();
+ }
+ }
+
+ return false;
+}
+
+void
+Preview::set_linked(LinkType link)
+{
+ link = (LinkType)(link & PREVIEW_LINK_ALL);
+
+ if (link != _linked)
+ {
+ _linked = link;
+
+ queue_draw();
+ }
+}
+
+LinkType
+Preview::get_linked() const
+{
+ return (LinkType)_linked;
+}
+
+void
+Preview::set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border)
+{
+ _view = view;
+
+ if ( size > PREVIEW_SIZE_LAST )
+ {
+ size = PREVIEW_SIZE_LAST;
+ }
+
+ _size = size;
+
+ if ( ratio > PREVIEW_MAX_RATIO )
+ {
+ ratio = PREVIEW_MAX_RATIO;
+ }
+
+ _ratio = ratio;
+ _border = border;
+
+ queue_draw();
+}
+
+Preview::Preview()
+ : _r(0x80),
+ _g(0x80),
+ _b(0xcc),
+ _scaledW(0),
+ _scaledH(0),
+ _hot(false),
+ _within(false),
+ _takesFocus(false),
+ _view(VIEW_TYPE_LIST),
+ _size(PREVIEW_SIZE_SMALL),
+ _ratio(100),
+ _border(BORDER_NONE),
+ _previewPixbuf(nullptr),
+ _scaled(nullptr),
+ _linked(PREVIEW_LINK_NONE)
+{
+ set_can_focus(true);
+ set_receives_default(true);
+
+ set_sensitive(true);
+
+ add_events(Gdk::BUTTON_PRESS_MASK
+ |Gdk::BUTTON_RELEASE_MASK
+ |Gdk::KEY_PRESS_MASK
+ |Gdk::KEY_RELEASE_MASK
+ |Gdk::FOCUS_CHANGE_MASK
+ |Gdk::ENTER_NOTIFY_MASK
+ |Gdk::LEAVE_NOTIFY_MASK );
+}
+
+} // namespace Widget
+} // 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/widget/preview.h b/src/ui/widget/preview.h
new file mode 100644
index 0000000..b455367
--- /dev/null
+++ b/src/ui/widget/preview.h
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005-2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef SEEN_EEK_PREVIEW_H
+#define SEEN_EEK_PREVIEW_H
+
+#include <gtkmm/drawingarea.h>
+
+/**
+ * @file
+ * Generic implementation of an object that can be shown by a preview.
+ */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum PreviewStyle {
+ PREVIEW_STYLE_ICON = 0,
+ PREVIEW_STYLE_PREVIEW,
+ PREVIEW_STYLE_NAME,
+ PREVIEW_STYLE_BLURB,
+ PREVIEW_STYLE_ICON_NAME,
+ PREVIEW_STYLE_ICON_BLURB,
+ PREVIEW_STYLE_PREVIEW_NAME,
+ PREVIEW_STYLE_PREVIEW_BLURB
+};
+
+enum ViewType {
+ VIEW_TYPE_LIST = 0,
+ VIEW_TYPE_GRID
+};
+
+enum PreviewSize {
+ PREVIEW_SIZE_TINY = 0,
+ PREVIEW_SIZE_SMALL,
+ PREVIEW_SIZE_MEDIUM,
+ PREVIEW_SIZE_BIG,
+ PREVIEW_SIZE_BIGGER,
+ PREVIEW_SIZE_HUGE
+};
+
+enum LinkType {
+ PREVIEW_LINK_NONE = 0,
+ PREVIEW_LINK_IN = 1,
+ PREVIEW_LINK_OUT = 2,
+ PREVIEW_LINK_OTHER = 4,
+ PREVIEW_FILL = 8,
+ PREVIEW_STROKE = 16,
+ PREVIEW_LINK_ALL = 31
+};
+
+enum BorderStyle {
+ BORDER_NONE = 0,
+ BORDER_SOLID,
+ BORDER_WIDE,
+ BORDER_SOLID_LAST_ROW,
+};
+
+class Preview : public Gtk::DrawingArea {
+private:
+ int _scaledW;
+ int _scaledH;
+
+ int _r;
+ int _g;
+ int _b;
+
+ bool _hot;
+ bool _within;
+ bool _takesFocus; ///< flag to grab focus when clicked
+ ViewType _view;
+ PreviewSize _size;
+ unsigned int _ratio;
+ LinkType _linked;
+ unsigned int _border;
+
+ Glib::RefPtr<Gdk::Pixbuf> _previewPixbuf;
+ Glib::RefPtr<Gdk::Pixbuf> _scaled;
+
+ // signals
+ sigc::signal<void> _signal_clicked;
+ sigc::signal<void, int> _signal_alt_clicked;
+
+ void size_request(GtkRequisition *req) const;
+
+protected:
+ void get_preferred_width_vfunc(int &minimal_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimal_height, int &natural_height) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+ bool on_button_press_event(GdkEventButton *button_event) override;
+ bool on_button_release_event(GdkEventButton *button_event) override;
+ bool on_enter_notify_event(GdkEventCrossing* event ) override;
+ bool on_leave_notify_event(GdkEventCrossing* event ) override;
+
+public:
+ Preview();
+ bool get_focus_on_click() const {return _takesFocus;}
+ void set_focus_on_click(bool focus_on_click) {_takesFocus = focus_on_click;}
+ LinkType get_linked() const;
+ void set_linked(LinkType link);
+ void set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border);
+ void set_color(int r, int g, int b);
+ void set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf);
+ static void set_size_mappings(guint count, GtkIconSize const* sizes);
+
+ decltype(_signal_clicked) signal_clicked() {return _signal_clicked;}
+ decltype(_signal_alt_clicked) signal_alt_clicked() {return _signal_alt_clicked;}
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* SEEN_EEK_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/widget/random.cpp b/src/ui/widget/random.cpp
new file mode 100644
index 0000000..495a778
--- /dev/null
+++ b/src/ui/widget/random.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "random.h"
+#include "ui/icon-loader.h"
+#include <glibmm/i18n.h>
+
+#include <gtkmm/button.h>
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, adjust, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+long Random::getStartSeed() const
+{
+ return startseed;
+}
+
+void Random::setStartSeed(long newseed)
+{
+ startseed = newseed;
+}
+
+void Random::addReseedButton()
+{
+ Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("randomize", Gtk::ICON_SIZE_BUTTON));
+ 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, &Random::onReseedButtonClick));
+ pButton->set_tooltip_text(_("Reseed the random number generator; this creates a different sequence of random numbers."));
+
+ pack_start(*pButton, Gtk::PACK_SHRINK, 0);
+}
+
+void
+Random::onReseedButtonClick()
+{
+ startseed = g_random_int();
+ signal_reseeded.emit();
+}
+
+} // namespace Widget
+} // 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/widget/random.h b/src/ui/widget/random.h
new file mode 100644
index 0000000..2648cb2
--- /dev/null
+++ b/src/ui/widget/random.h
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_RANDOM_H
+#define INKSCAPE_UI_WIDGET_RANDOM_H
+
+#include "scalar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values. It adds an extra
+ * number called "startseed", that is not UI edittable, but should be put in SVG.
+ * This does NOT generate a random number, but provides merely the saving of
+ * the startseed value.
+ */
+class Random : public Scalar
+{
+public:
+
+ /**
+ * Construct a Random scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Gets the startseed.
+ */
+ long getStartSeed() const;
+
+ /**
+ * Sets the startseed number.
+ */
+ void setStartSeed(long newseed);
+
+ sigc::signal <void> signal_reseeded;
+
+protected:
+ long startseed;
+
+private:
+
+ /**
+ * Add reseed button to the widget.
+ */
+ void addReseedButton();
+
+ void onReseedButtonClick();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RANDOM_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/widget/registered-enums.h b/src/ui/widget/registered-enums.h
new file mode 100644
index 0000000..b0cc199
--- /dev/null
+++ b/src/ui/widget/registered-enums.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+#define INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/registered-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class RegisteredEnum : public RegisteredWidget< LabelledComboBoxEnum<E> >
+{
+public:
+ ~RegisteredEnum() override {
+ _changed_connection.disconnect();
+ }
+
+ RegisteredEnum ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const Util::EnumDataConverter<E>& c,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ bool sorted = true )
+ : RegisteredWidget< LabelledComboBoxEnum<E> >(label, tip, c, (const Glib::ustring &)"", (const Glib::ustring &)"", true, sorted)
+ {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::init_parent(key, wr, repr_in, doc_in);
+ _changed_connection = combobox()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredEnum::on_changed));
+ }
+
+ void set_active_by_id (E id) {
+ combobox()->set_active_by_id(id);
+ };
+
+ void set_active_by_key (const Glib::ustring& key) {
+ combobox()->set_active_by_key(key);
+ }
+
+ inline const Util::EnumData<E>* get_active_data() {
+ combobox()->get_active_data();
+ }
+
+ ComboBoxEnum<E> * combobox() {
+ return LabelledComboBoxEnum<E>::getCombobox();
+ }
+
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed() {
+ if (combobox()->setProgrammatically) {
+ combobox()->setProgrammatically = false;
+ return;
+ }
+
+ if (RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->isUpdating())
+ return;
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (true);
+
+ const Util::EnumData<E>* data = combobox()->get_active_data();
+ if (data) {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::write_to_xml(data->key.c_str());
+ }
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (false);
+ }
+};
+
+}
+}
+}
+
+#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/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp
new file mode 100644
index 0000000..bd62b73
--- /dev/null
+++ b/src/ui/widget/registered-widget.cpp
@@ -0,0 +1,845 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "registered-widget.h"
+
+#include <gtkmm/radiobutton.h>
+
+#include "verbs.h"
+
+#include "object/sp-root.h"
+
+#include "svg/svg-color.h"
+#include "svg/stringstream.h"
+
+#include "widgets/spinbutton-events.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*#########################################
+ * Registered CHECKBUTTON
+ */
+
+RegisteredCheckButton::~RegisteredCheckButton()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredCheckButton::RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *active_str, char const *inactive_str)
+ : RegisteredWidget<Gtk::CheckButton>()
+ , _active_str(active_str)
+ , _inactive_str(inactive_str)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ set_tooltip_text (tip);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup(label);
+ l->set_use_underline (true);
+ add (*manage (l));
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredCheckButton::on_toggled));
+}
+
+void
+RegisteredCheckButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredCheckButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? _active_str : _inactive_str);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TOGGLEBUTTON
+ */
+
+RegisteredToggleButton::~RegisteredToggleButton()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredToggleButton::RegisteredToggleButton (const Glib::ustring& /*label*/, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *icon_active, char const *icon_inactive)
+ : RegisteredWidget<Gtk::ToggleButton>()
+{
+ init_parent(key, wr, repr_in, doc_in);
+ setProgrammatically = false;
+ set_tooltip_text (tip);
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredToggleButton::on_toggled));
+}
+
+void
+RegisteredToggleButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredToggleButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? "true" : "false");
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered UNITMENU
+ */
+
+RegisteredUnitMenu::~RegisteredUnitMenu()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredUnitMenu::RegisteredUnitMenu (const Glib::ustring& label, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Labelled> (label, "" /*tooltip*/, new UnitMenu())
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ getUnitMenu()->setUnitType (UNIT_TYPE_LINEAR);
+ _changed_connection = getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredUnitMenu::on_changed));
+}
+
+void
+RegisteredUnitMenu::setUnit (Glib::ustring unit)
+{
+ getUnitMenu()->setUnit(unit);
+}
+
+void
+RegisteredUnitMenu::on_changed()
+{
+ if (_wr->isUpdating())
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << getUnitMenu()->getUnitAbbr();
+
+ _wr->setUpdating (true);
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALARUNIT
+ */
+
+RegisteredScalarUnit::~RegisteredScalarUnit()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalarUnit::RegisteredScalarUnit (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, const RegisteredUnitMenu &rum, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in, RSU_UserUnits user_units)
+ : RegisteredWidget<ScalarUnit>(label, tip, UNIT_TYPE_LINEAR, "", "", rum.getUnitMenu()),
+ _um(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ initScalar (-1e6, 1e6);
+ setUnit (rum.getUnitMenu()->getUnitAbbr());
+ setDigits (2);
+ _um = rum.getUnitMenu();
+ _user_units = user_units;
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalarUnit::on_value_changed));
+}
+
+
+void
+RegisteredScalarUnit::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ if (_user_units != RSU_none) {
+ // Output length in 'user units', taking into account scale in 'x' or 'y'.
+ double scale = 1.0;
+ if (doc) {
+ SPRoot *root = doc->getRoot();
+ if (root->viewBox_set) {
+ // check to see if scaling is uniform
+ if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) {
+ scale = (root->viewBox.width() / root->width.computed + root->viewBox.height() / root->height.computed)/2.0;
+ } else if (_user_units == RSU_x) {
+ scale = root->viewBox.width() / root->width.computed;
+ } else {
+ scale = root->viewBox.height() / root->height.computed;
+ }
+ }
+ }
+ os << getValue("px") * scale;
+ } else {
+ // Output using unit identifiers.
+ os << getValue("");
+ if (_um)
+ os << _um->getUnitAbbr();
+ }
+
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALAR
+ */
+
+RegisteredScalar::~RegisteredScalar()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalar::RegisteredScalar ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Scalar>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalar::on_value_changed));
+}
+
+void
+RegisteredScalar::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ //Force exact 0 if decimals over to 6
+ double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue();
+ os << val;
+ //TODO: Test is ok remove this sensitives
+ //also removed in registered text and in registered random
+ //set_sensitive(false);
+ write_to_xml(os.str().c_str());
+ //set_sensitive(true);
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered TEXT
+ */
+
+RegisteredText::~RegisteredText()
+{
+ _activate_connection.disconnect();
+}
+
+RegisteredText::RegisteredText ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Text>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ _activate_connection = signal_activate().connect (sigc::mem_fun (*this, &RegisteredText::on_activate));
+}
+
+void
+RegisteredText::on_activate()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+ Glib::ustring str(getText());
+ Inkscape::SVGOStringStream os;
+ os << str;
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered COLORPICKER
+ */
+
+RegisteredColorPicker::RegisteredColorPicker(const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in,
+ SPDocument *doc_in)
+ : RegisteredWidget<LabelledColorPicker> (label, title, tip, 0, true)
+{
+ init_parent("", wr, repr_in, doc_in);
+
+ _ckey = ckey;
+ _akey = akey;
+ _changed_connection = connectChanged (sigc::mem_fun (*this, &RegisteredColorPicker::on_changed));
+}
+
+RegisteredColorPicker::~RegisteredColorPicker()
+{
+ _changed_connection.disconnect();
+}
+
+void
+RegisteredColorPicker::setRgba32 (guint32 rgba)
+{
+ LabelledColorPicker::setRgba32 (rgba);
+}
+
+void
+RegisteredColorPicker::closeWindow()
+{
+ LabelledColorPicker::closeWindow();
+}
+
+void
+RegisteredColorPicker::on_changed (guint32 rgba)
+{
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ // Use local repr here. When repr is specified, use that one, but
+ // if repr==NULL, get the repr of namedview of active desktop.
+ Inkscape::XML::Node *local_repr = repr;
+ SPDocument *local_doc = doc;
+ if (!local_repr) {
+ // no repr specified, use active desktop's namedview's repr
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (!dt)
+ return;
+ local_repr = dt->getNamedView()->getRepr();
+ local_doc = dt->getDocument();
+ }
+ gchar c[32];
+ if (_akey == _ckey + "_opacity_LPE") { //For LPE parameter we want stored with alpha
+ sprintf(c, "#%08x", rgba);
+ } else {
+ sp_svg_write_color(c, sizeof(c), rgba);
+ }
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ local_repr->setAttribute(_ckey, c);
+ sp_repr_set_css_double(local_repr, _akey.c_str(), (rgba & 0xff) / 255.0);
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+
+ local_doc->setModifiedSinceSave();
+ DocumentUndo::done(local_doc, SP_VERB_NONE,
+ /* TODO: annotate */ "registered-widget.cpp: RegisteredColorPicker::on_changed");
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SUFFIXEDINTEGER
+ */
+
+RegisteredSuffixedInteger::~RegisteredSuffixedInteger()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredSuffixedInteger::RegisteredSuffixedInteger (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& suffix, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Scalar>(label, tip, 0, suffix),
+ setProgrammatically(false)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (0, 1e6);
+ setDigits (0);
+ setIncrements(1, 10);
+
+ _changed_connection = signal_value_changed().connect (sigc::mem_fun(*this, &RegisteredSuffixedInteger::on_value_changed));
+}
+
+void
+RegisteredSuffixedInteger::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered RADIOBUTTONPAIR
+ */
+
+RegisteredRadioButtonPair::~RegisteredRadioButtonPair()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredRadioButtonPair::RegisteredRadioButtonPair (const Glib::ustring& label,
+ const Glib::ustring& label1, const Glib::ustring& label2,
+ const Glib::ustring& tip1, const Glib::ustring& tip2,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Gtk::HBox>(),
+ _rb1(nullptr),
+ _rb2(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ add(*Gtk::manage(new Gtk::Label(label)));
+ _rb1 = Gtk::manage(new Gtk::RadioButton(label1, true));
+ add (*_rb1);
+ Gtk::RadioButtonGroup group = _rb1->get_group();
+ _rb2 = Gtk::manage(new Gtk::RadioButton(group, label2, true));
+ add (*_rb2);
+ _rb2->set_active();
+ _rb1->set_tooltip_text(tip1);
+ _rb2->set_tooltip_text(tip2);
+ _changed_connection = _rb1->signal_toggled().connect (sigc::mem_fun (*this, &RegisteredRadioButtonPair::on_value_changed));
+}
+
+void
+RegisteredRadioButtonPair::setValue (bool second)
+{
+ if (!_rb1 || !_rb2)
+ return;
+
+ setProgrammatically = true;
+ if (second) {
+ _rb2->set_active();
+ } else {
+ _rb1->set_active();
+ }
+}
+
+void
+RegisteredRadioButtonPair::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ bool second = _rb2->get_active();
+ write_to_xml(second ? "true" : "false");
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered POINT
+ */
+
+RegisteredPoint::~RegisteredPoint()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredPoint::RegisteredPoint ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed));
+}
+
+void
+RegisteredPoint::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getXValue() << "," << getYValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TRANSFORMEDPOINT
+ */
+
+RegisteredTransformedPoint::~RegisteredTransformedPoint()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredTransformedPoint::RegisteredTransformedPoint ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip),
+ to_svg(Geom::identity())
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed));
+}
+
+void
+RegisteredTransformedPoint::setValue(Geom::Point const & p)
+{
+ Geom::Point new_p = p * to_svg.inverse();
+ Point::setValue(new_p); // the Point widget should display things in canvas coordinates
+}
+
+void
+RegisteredTransformedPoint::setTransform(Geom::Affine const & canvas_to_svg)
+{
+ // check if matrix is singular / has inverse
+ if ( ! canvas_to_svg.isSingular() ) {
+ to_svg = canvas_to_svg;
+ } else {
+ // set back to default
+ to_svg = Geom::identity();
+ }
+}
+
+void
+RegisteredTransformedPoint::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Geom::Point pos = getValue() * to_svg;
+
+ Inkscape::SVGOStringStream os;
+ os << pos;
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TRANSFORMEDPOINT
+ */
+
+RegisteredVector::~RegisteredVector()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredVector::RegisteredVector ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip),
+ _polar_coords(false)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed));
+}
+
+void
+RegisteredVector::setValue(Geom::Point const & p)
+{
+ if (!_polar_coords) {
+ Point::setValue(p);
+ } else {
+ Geom::Point polar;
+ polar[Geom::X] = atan2(p) *180/M_PI;
+ polar[Geom::Y] = p.length();
+ Point::setValue(polar);
+ }
+}
+
+void
+RegisteredVector::setValue(Geom::Point const & p, Geom::Point const & origin)
+{
+ RegisteredVector::setValue(p);
+ _origin = origin;
+}
+
+void RegisteredVector::setPolarCoords(bool polar_coords)
+{
+ _polar_coords = polar_coords;
+ if (polar_coords) {
+ xwidget.setLabelText("Angle:");
+ ywidget.setLabelText("Distance:");
+ } else {
+ xwidget.setLabelText("X:");
+ ywidget.setLabelText("Y:");
+ }
+}
+
+void
+RegisteredVector::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Geom::Point origin = _origin;
+ Geom::Point vector = getValue();
+ if (_polar_coords) {
+ vector = Geom::Point::polar(vector[Geom::X]*M_PI/180, vector[Geom::Y]);
+ }
+
+ Inkscape::SVGOStringStream os;
+ os << origin << " , " << vector;
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered RANDOM
+ */
+
+RegisteredRandom::~RegisteredRandom()
+{
+ _value_changed_connection.disconnect();
+ _reseeded_connection.disconnect();
+}
+
+RegisteredRandom::RegisteredRandom ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Random> (label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredRandom::on_value_changed));
+ _reseeded_connection = signal_reseeded.connect(sigc::mem_fun(*this, &RegisteredRandom::on_value_changed));
+}
+
+void
+RegisteredRandom::setValue (double val, long startseed)
+{
+ Scalar::setValue (val);
+ setStartSeed(startseed);
+}
+
+void
+RegisteredRandom::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ //Force exact 0 if decimals over to 6
+ double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue();
+ os << val << ';' << getStartSeed();
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered FONT-BUTTON
+ */
+
+RegisteredFontButton::~RegisteredFontButton()
+{
+ _signal_font_set.disconnect();
+}
+
+RegisteredFontButton::RegisteredFontButton ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<FontButton>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+ _signal_font_set = signal_font_value_changed().connect (sigc::mem_fun (*this, &RegisteredFontButton::on_value_changed));
+}
+
+void
+RegisteredFontButton::setValue (Glib::ustring fontspec)
+{
+ FontButton::setValue(fontspec);
+}
+
+void
+RegisteredFontButton::on_value_changed()
+{
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+} // 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/widget/registered-widget.h b/src/ui/widget/registered-widget.h
new file mode 100644
index 0000000..d0b728a
--- /dev/null
+++ b/src/ui/widget/registered-widget.h
@@ -0,0 +1,458 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_
+#define INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_
+
+#include <2geom/affine.h>
+#include "xml/node.h"
+#include "registry.h"
+
+#include "ui/widget/scalar.h"
+#include "ui/widget/scalar-unit.h"
+#include "ui/widget/point.h"
+#include "ui/widget/text.h"
+#include "ui/widget/random.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/font-button.h"
+#include "ui/widget/color-picker.h"
+#include "inkscape.h"
+
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+#include "object/sp-namedview.h"
+
+#include <gtkmm/checkbutton.h>
+
+class SPDocument;
+
+namespace Gtk {
+ class HScale;
+ class RadioButton;
+ class SpinButton;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+template <class W>
+class RegisteredWidget : public W {
+public:
+ void set_undo_parameters(const unsigned int _event_type, Glib::ustring _event_description)
+ {
+ event_type = _event_type;
+ event_description = _event_description;
+ write_undo = true;
+ }
+ void set_xml_target(Inkscape::XML::Node *xml_node, SPDocument *document)
+ {
+ repr = xml_node;
+ doc = document;
+ }
+
+ bool is_updating() {if (_wr) return _wr->isUpdating(); else return false;}
+
+protected:
+ RegisteredWidget() : W() { construct(); }
+ template< typename A >
+ explicit RegisteredWidget( A& a ): W( a ) { construct(); }
+ template< typename A, typename B >
+ RegisteredWidget( A& a, B& b ): W( a, b ) { construct(); }
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C* c ): W( a, b, c ) { construct(); }
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C& c ): W( a, b, c ) { construct(); }
+ template< typename A, typename B, typename C, typename D >
+ RegisteredWidget( A& a, B& b, C c, D d ): W( a, b, c, d ) { construct(); }
+ template< typename A, typename B, typename C, typename D, typename E >
+ RegisteredWidget( A& a, B& b, C& c, D d, E e ): W( a, b, c, d, e ) { construct(); }
+ template< typename A, typename B, typename C, typename D, typename E , typename F>
+ RegisteredWidget( A& a, B& b, C c, D& d, E& e, F* f): W( a, b, c, d, e, f) { construct(); }
+ template< typename A, typename B, typename C, typename D, typename E , typename F, typename G>
+ RegisteredWidget( A& a, B& b, C& c, D& d, E& e, F f, G& g): W( a, b, c, d, e, f, g) { construct(); }
+
+ ~RegisteredWidget() override = default;;
+
+ void init_parent(const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ {
+ _wr = &wr;
+ _key = key;
+ repr = repr_in;
+ doc = doc_in;
+ if (repr && !doc) // doc cannot be NULL when repr is not NULL
+ g_warning("Initialization of registered widget using defined repr but with doc==NULL");
+ }
+
+ void write_to_xml(const char * svgstr)
+ {
+ // Use local repr here. When repr is specified, use that one, but
+ // if repr==NULL, get the repr of namedview of active desktop.
+ Inkscape::XML::Node *local_repr = repr;
+ SPDocument *local_doc = doc;
+ if (!local_repr) {
+ // no repr specified, use active desktop's namedview's repr
+ SPDesktop* dt = SP_ACTIVE_DESKTOP;
+ local_repr = reinterpret_cast<SPObject *>(dt->getNamedView())->getRepr();
+ local_doc = dt->getDocument();
+ }
+
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ const char * svgstr_old = local_repr->attribute(_key.c_str());
+ if (!write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ }
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+ if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) {
+ local_doc->setModifiedSinceSave();
+ }
+
+ if (write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ DocumentUndo::done(local_doc, event_type, event_description);
+ }
+ }
+
+ Registry * _wr;
+ Glib::ustring _key;
+ Inkscape::XML::Node * repr;
+ SPDocument * doc;
+ unsigned int event_type;
+ Glib::ustring event_description;
+ bool write_undo;
+
+private:
+ void construct() {
+ _wr = nullptr;
+ repr = nullptr;
+ doc = nullptr;
+ write_undo = false;
+ event_type = 0; //SP_VERB_INVALID
+ }
+};
+
+//#######################################################
+
+class RegisteredCheckButton : public RegisteredWidget<Gtk::CheckButton> {
+public:
+ ~RegisteredCheckButton() override;
+ RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=false, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *active_str = "true", char const *inactive_str = "false");
+
+ void setActive (bool);
+
+ std::list<Gtk::Widget*> _slavewidgets;
+
+ // a slave button is only sensitive when the master button is active
+ // i.e. a slave button is greyed-out when the master button is not checked
+
+ void setSlaveWidgets(std::list<Gtk::Widget*> btns) {
+ _slavewidgets = btns;
+ }
+
+ bool setProgrammatically; // true if the value was set by setActive, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ char const *_active_str, *_inactive_str;
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> {
+public:
+ ~RegisteredToggleButton() override;
+ RegisteredToggleButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=true, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *icon_active = "true", char const *icon_inactive = "false");
+
+ void setActive (bool);
+
+ std::list<Gtk::Widget*> _slavewidgets;
+
+ // a slave button is only sensitive when the master button is active
+ // i.e. a slave button is greyed-out when the master button is not checked
+
+ void setSlaveWidgets(std::list<Gtk::Widget*> btns) {
+ _slavewidgets = btns;
+ }
+
+ bool setProgrammatically; // true if the value was set by setActive, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredUnitMenu : public RegisteredWidget<Labelled> {
+public:
+ ~RegisteredUnitMenu() override;
+ RegisteredUnitMenu ( const Glib::ustring& label,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ void setUnit (const Glib::ustring);
+ Unit const * getUnit() const { return static_cast<UnitMenu*>(_widget)->getUnit(); };
+ UnitMenu* getUnitMenu() const { return static_cast<UnitMenu*>(_widget); };
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed();
+};
+
+// Allow RegisteredScalarUnit to output lengths in 'user units' (which may have direction dependent
+// scale factors).
+enum RSU_UserUnits {
+ RSU_none,
+ RSU_x,
+ RSU_y
+};
+
+class RegisteredScalarUnit : public RegisteredWidget<ScalarUnit> {
+public:
+ ~RegisteredScalarUnit() override;
+ RegisteredScalarUnit ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const RegisteredUnitMenu &rum,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ RSU_UserUnits _user_units = RSU_none );
+
+protected:
+ sigc::connection _value_changed_connection;
+ UnitMenu *_um;
+ void on_value_changed();
+ RSU_UserUnits _user_units;
+};
+
+class RegisteredScalar : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredScalar() override;
+ RegisteredScalar (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+protected:
+ sigc::connection _value_changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredText : public RegisteredWidget<Text> {
+public:
+ ~RegisteredText() override;
+ RegisteredText (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+protected:
+ sigc::connection _activate_connection;
+ void on_activate();
+};
+
+class RegisteredColorPicker : public RegisteredWidget<LabelledColorPicker> {
+public:
+ ~RegisteredColorPicker() override;
+
+ RegisteredColorPicker (const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setRgba32 (guint32);
+ void closeWindow();
+
+protected:
+ Glib::ustring _ckey, _akey;
+ void on_changed (guint32);
+ sigc::connection _changed_connection;
+};
+
+class RegisteredSuffixedInteger : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredSuffixedInteger() override;
+ RegisteredSuffixedInteger ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& suffix,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ sigc::connection _changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredRadioButtonPair : public RegisteredWidget<Gtk::HBox> {
+public:
+ ~RegisteredRadioButtonPair() override;
+ RegisteredRadioButtonPair ( const Glib::ustring& label,
+ const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ void setValue (bool second);
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+protected:
+ Gtk::RadioButton *_rb1, *_rb2;
+ sigc::connection _changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredPoint : public RegisteredWidget<Point> {
+public:
+ ~RegisteredPoint() override;
+ RegisteredPoint ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+};
+
+
+class RegisteredTransformedPoint : public RegisteredWidget<Point> {
+public:
+ ~RegisteredTransformedPoint() override;
+ RegisteredTransformedPoint ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ // redefine setValue, because transform must be applied
+ void setValue(Geom::Point const & p);
+
+ void setTransform(Geom::Affine const & canvas_to_svg);
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+
+ Geom::Affine to_svg;
+};
+
+
+class RegisteredVector : public RegisteredWidget<Point> {
+public:
+ ~RegisteredVector() override;
+ RegisteredVector (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ // redefine setValue, because transform must be applied
+ void setValue(Geom::Point const & p);
+ void setValue(Geom::Point const & p, Geom::Point const & origin);
+
+ /**
+ * Changes the widgets text to polar coordinates. The SVG output will still be a normal carthesian vector.
+ * Careful: when calling getValue(), the return value's X-coord will be the angle, Y-value will be the distance/length.
+ * After changing the coords type (polar/non-polar), the value has to be reset (setValue).
+ */
+ void setPolarCoords(bool polar_coords = true);
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+
+ Geom::Point _origin;
+ bool _polar_coords;
+};
+
+
+class RegisteredRandom : public RegisteredWidget<Random> {
+public:
+ ~RegisteredRandom() override;
+ RegisteredRandom ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setValue (double val, long startseed);
+
+protected:
+ sigc::connection _value_changed_connection;
+ sigc::connection _reseeded_connection;
+ void on_value_changed();
+};
+
+class RegisteredFontButton : public RegisteredWidget<FontButton> {
+public:
+ ~RegisteredFontButton() override;
+ RegisteredFontButton ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setValue (Glib::ustring fontspec);
+
+protected:
+ sigc::connection _signal_font_set;
+ void on_value_changed();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__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/widget/registry.cpp b/src/ui/widget/registry.cpp
new file mode 100644
index 0000000..2834007
--- /dev/null
+++ b/src/ui/widget/registry.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "registry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+Registry::Registry() : _updating(false) {}
+
+Registry::~Registry() = default;
+
+bool
+Registry::isUpdating()
+{
+ return _updating;
+}
+
+void
+Registry::setUpdating (bool upd)
+{
+ _updating = upd;
+}
+
+//====================================================
+
+
+} // 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/widget/registry.h b/src/ui/widget/registry.h
new file mode 100644
index 0000000..190aaac
--- /dev/null
+++ b/src/ui/widget/registry.h
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_REGISTRY__H
+#define INKSCAPE_UI_WIDGET_REGISTRY__H
+
+namespace Gtk {
+ class Object;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry {
+public:
+ Registry();
+ ~Registry();
+
+ bool isUpdating();
+ void setUpdating (bool);
+
+protected:
+ bool _updating;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Widget
+
+#endif // INKSCAPE_UI_WIDGET_REGISTRY__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/widget/rendering-options.cpp b/src/ui/widget/rendering-options.cpp
new file mode 100644
index 0000000..549f494
--- /dev/null
+++ b/src/ui/widget/rendering-options.cpp
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+#include "preferences.h"
+#include "rendering-options.h"
+#include "util/units.h"
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void RenderingOptions::_toggled()
+{
+ _frame_bitmap.set_sensitive(as_bitmap());
+}
+
+RenderingOptions::RenderingOptions () :
+ Gtk::VBox (),
+ _frame_backends ( Glib::ustring(_("Backend")) ),
+ _radio_vector ( Glib::ustring(_("Vector")) ),
+ _radio_bitmap ( Glib::ustring(_("Bitmap")) ),
+ _frame_bitmap ( Glib::ustring(_("Bitmap options")) ),
+ _dpi( _("DPI"),
+ Glib::ustring(_("Preferred resolution of rendering, "
+ "in dots per inch.")),
+ 1,
+ Glib::ustring(""), Glib::ustring(""),
+ false)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // set up tooltips
+ _radio_vector.set_tooltip_text(
+ _("Render using Cairo vector operations. "
+ "The resulting image is usually smaller in file "
+ "size and can be arbitrarily scaled, but some "
+ "filter effects will not be correctly rendered."));
+ _radio_bitmap.set_tooltip_text(
+ _("Render everything as bitmap. The resulting image "
+ "is usually larger in file size and cannot be "
+ "arbitrarily scaled without quality loss, but all "
+ "objects will be rendered exactly as displayed."));
+
+ set_border_width(2);
+
+ Gtk::RadioButtonGroup group = _radio_vector.get_group ();
+ _radio_bitmap.set_group (group);
+ _radio_bitmap.signal_toggled().connect(sigc::mem_fun(*this, &RenderingOptions::_toggled));
+
+ // default to vector operations
+ if (prefs->getBool("/dialogs/printing/asbitmap", false)) {
+ _radio_bitmap.set_active();
+ } else {
+ _radio_vector.set_active();
+ }
+
+ // configure default DPI
+ _dpi.setRange(Inkscape::Util::Quantity::convert(1, "in", "pt"),2400.0);
+ _dpi.setValue(prefs->getDouble("/dialogs/printing/dpi",
+ Inkscape::Util::Quantity::convert(1, "in", "pt")));
+ _dpi.setIncrements(1.0,10.0);
+ _dpi.setDigits(0);
+ _dpi.update();
+
+ // fill frames
+ Gtk::VBox *box_vector = Gtk::manage( new Gtk::VBox () );
+ box_vector->set_border_width (2);
+ box_vector->add (_radio_vector);
+ box_vector->add (_radio_bitmap);
+ _frame_backends.add (*box_vector);
+
+ Gtk::HBox *box_bitmap = Gtk::manage( new Gtk::HBox () );
+ box_bitmap->set_border_width (2);
+ box_bitmap->add (_dpi);
+ _frame_bitmap.add (*box_bitmap);
+
+ // fill up container
+ add (_frame_backends);
+ add (_frame_bitmap);
+
+ // initialize states
+ _toggled();
+
+ show_all_children ();
+}
+
+bool
+RenderingOptions::as_bitmap ()
+{
+ return _radio_bitmap.get_active();
+}
+
+double
+RenderingOptions::bitmap_dpi ()
+{
+ return _dpi.getValue();
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ 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/widget/rendering-options.h b/src/ui/widget/rendering-options.h
new file mode 100644
index 0000000..2e10ff3
--- /dev/null
+++ b/src/ui/widget/rendering-options.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+#define INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+
+#include "scalar.h"
+
+#include <gtkmm/frame.h>
+#include <gtkmm/radiobutton.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container for selecting rendering options.
+ */
+class RenderingOptions : public Gtk::VBox
+{
+public:
+
+ /**
+ * Construct a Rendering Options widget.
+ */
+ RenderingOptions();
+
+ bool as_bitmap(); // should we render as a bitmap?
+ double bitmap_dpi(); // at what DPI should we render the bitmap?
+
+protected:
+ // Radio buttons to select desired rendering
+ Gtk::Frame _frame_backends;
+ Gtk::RadioButton _radio_vector;
+ Gtk::RadioButton _radio_bitmap;
+
+ // Bitmap options
+ Gtk::Frame _frame_bitmap;
+ Scalar _dpi; // DPI of bitmap to render
+
+ // callback for bitmap button
+ void _toggled();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ 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/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp
new file mode 100644
index 0000000..639f8d1
--- /dev/null
+++ b/src/ui/widget/rotateable.cpp
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+#include "rotateable.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Rotateable::Rotateable():
+ axis(-M_PI/4),
+ maxdecl(M_PI/4)
+{
+ dragging = false;
+ working = false;
+ scrolling = false;
+ modifier = 0;
+ current_axis = axis;
+
+ signal_button_press_event().connect(sigc::mem_fun(*this, &Rotateable::on_click));
+ signal_motion_notify_event().connect(sigc::mem_fun(*this, &Rotateable::on_motion));
+ signal_button_release_event().connect(sigc::mem_fun(*this, &Rotateable::on_release));
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &Rotateable::on_scroll));
+
+}
+
+bool Rotateable::on_click(GdkEventButton *event) {
+ if (event->button == 1) {
+ drag_started_x = event->x;
+ drag_started_y = event->y;
+ modifier = get_single_modifier(modifier, event->state);
+ dragging = true;
+ working = false;
+ current_axis = axis;
+ return true;
+ }
+ return false;
+}
+
+guint Rotateable::get_single_modifier(guint old, guint state) {
+
+ if (old == 0 || old == 3) {
+ if (state & GDK_CONTROL_MASK)
+ return 1; // ctrl
+ if (state & GDK_SHIFT_MASK)
+ return 2; // shift
+ if (state & GDK_MOD1_MASK)
+ return 3; // alt
+ return 0;
+ } else {
+ if (!(state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) {
+ if (state & GDK_MOD1_MASK)
+ return 3; // alt
+ else
+ return 0; // none
+ }
+ if (old == 1) {
+ if (state & GDK_SHIFT_MASK && !(state & GDK_CONTROL_MASK))
+ return 2; // shift
+ if (state & GDK_MOD1_MASK && !(state & GDK_CONTROL_MASK))
+ return 3; // alt
+ return 1;
+ }
+ if (old == 2) {
+ if (state & GDK_CONTROL_MASK && !(state & GDK_SHIFT_MASK))
+ return 1; // ctrl
+ if (state & GDK_MOD1_MASK && !(state & GDK_SHIFT_MASK))
+ return 3; // alt
+ return 2;
+ }
+ return old;
+ }
+}
+
+
+bool Rotateable::on_motion(GdkEventMotion *event) {
+ if (dragging) {
+ double dist = Geom::L2(Geom::Point(event->x, event->y) - Geom::Point(drag_started_x, drag_started_y));
+ double angle = atan2(event->y - drag_started_y, event->x - drag_started_x);
+ if (dist > 20) {
+ working = true;
+ double force = CLAMP (-(angle - current_axis)/maxdecl, -1, 1);
+ if (fabs(force) < 0.002)
+ force = 0; // snap to zero
+ if (modifier != get_single_modifier(modifier, event->state)) {
+ // user has switched modifiers in mid drag, close past drag and start a new
+ // one, redefining axis temporarily
+ do_release(force, modifier);
+ current_axis = angle;
+ modifier = get_single_modifier(modifier, event->state);
+ } else {
+ do_motion(force, modifier);
+ }
+ }
+ Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK);
+ return true;
+ }
+ return false;
+}
+
+
+bool Rotateable::on_release(GdkEventButton *event) {
+ if (dragging && working) {
+ double angle = atan2(event->y - drag_started_y, event->x - drag_started_x);
+ double force = CLAMP(-(angle - current_axis) / maxdecl, -1, 1);
+ if (fabs(force) < 0.002)
+ force = 0; // snap to zero
+ do_release(force, modifier);
+ current_axis = axis;
+ dragging = false;
+ working = false;
+ return true;
+ }
+ dragging = false;
+ working = false;
+ return false;
+}
+
+bool Rotateable::on_scroll(GdkEventScroll* event)
+{
+ double change = 0.0;
+
+ if (event->direction == GDK_SCROLL_UP) {
+ change = 1.0;
+ } else if (event->direction == GDK_SCROLL_DOWN) {
+ change = -1.0;
+ } else if (event->direction == GDK_SCROLL_SMOOTH) {
+ double delta_y_clamped = CLAMP(event->delta_y, -1.0, 1.0); // values > 1 result in excessive changes
+ change = 1.0 * -delta_y_clamped;
+ } else {
+ return FALSE;
+ }
+
+ drag_started_x = event->x;
+ drag_started_y = event->y;
+ modifier = get_single_modifier(modifier, event->state);
+ dragging = false;
+ working = false;
+ scrolling = true;
+ current_axis = axis;
+
+ do_scroll(change, modifier);
+
+ dragging = false;
+ working = false;
+ scrolling = false;
+
+ return TRUE;
+}
+
+Rotateable::~Rotateable() = default;
+
+
+
+} // namespace Widget
+} // 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/widget/rotateable.h b/src/ui/widget/rotateable.h
new file mode 100644
index 0000000..c174a09
--- /dev/null
+++ b/src/ui/widget/rotateable.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_ROTATEABLE_H
+#define INKSCAPE_UI_ROTATEABLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Widget adjustable by dragging it to rotate away from a zero-change axis.
+ */
+class Rotateable: public Gtk::EventBox
+{
+public:
+ Rotateable();
+
+ ~Rotateable() override;
+
+ bool on_click(GdkEventButton *event);
+ bool on_motion(GdkEventMotion *event);
+ bool on_release(GdkEventButton *event);
+ bool on_scroll(GdkEventScroll* event);
+
+ double axis;
+ double current_axis;
+ double maxdecl;
+ bool scrolling;
+
+private:
+ double drag_started_x;
+ double drag_started_y;
+ guint modifier;
+ bool dragging;
+ bool working;
+
+ guint get_single_modifier(guint old, guint state);
+
+ virtual void do_motion (double /*by*/, guint /*state*/) {}
+ virtual void do_release (double /*by*/, guint /*state*/) {}
+ virtual void do_scroll (double /*by*/, guint /*state*/) {}
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_ROTATEABLE_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/widget/scalar-unit.cpp b/src/ui/widget/scalar-unit.cpp
new file mode 100644
index 0000000..2b6d001
--- /dev/null
+++ b/src/ui/widget/scalar-unit.cpp
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Derek P. Moore <derekm@hackunix.org>
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2004-2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scalar-unit.h"
+#include "spinbutton.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ UnitType unit_type,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ UnitMenu *unit_menu,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic),
+ _unit_menu(unit_menu),
+ _hundred_percent(0),
+ _absolute_is_increment(false),
+ _percentage_is_increment(false)
+{
+ if (_unit_menu == nullptr) {
+ _unit_menu = new UnitMenu();
+ g_assert(_unit_menu);
+ _unit_menu->setUnitType(unit_type);
+
+ remove(*_widget);
+ Gtk::Box *widget_holder = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 6);
+ widget_holder->pack_start(*_widget, Gtk::PACK_SHRINK);
+ widget_holder->pack_start(*Gtk::manage(_unit_menu), Gtk::PACK_SHRINK);
+ pack_start(*Gtk::manage(widget_holder), Gtk::PACK_SHRINK);
+ }
+ _unit_menu->signal_changed()
+ .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed));
+
+ static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu);
+
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ ScalarUnit &take_unitmenu,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic),
+ _unit_menu(take_unitmenu._unit_menu),
+ _hundred_percent(0),
+ _absolute_is_increment(false),
+ _percentage_is_increment(false)
+{
+ _unit_menu->signal_changed()
+ .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed));
+
+ static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu);
+
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+
+void ScalarUnit::initScalar(double min_value, double max_value)
+{
+ g_assert(_unit_menu != nullptr);
+ Scalar::setDigits(_unit_menu->getDefaultDigits());
+ Scalar::setIncrements(_unit_menu->getDefaultStep(),
+ _unit_menu->getDefaultPage());
+ Scalar::setRange(min_value, max_value);
+}
+
+bool ScalarUnit::setUnit(Glib::ustring const &unit)
+{
+ g_assert(_unit_menu != nullptr);
+ // First set the unit
+ if (!_unit_menu->setUnit(unit)) {
+ return false;
+ }
+ lastUnits = unit;
+ return true;
+}
+
+void ScalarUnit::setUnitType(UnitType unit_type)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->setUnitType(unit_type);
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+void ScalarUnit::resetUnitType(UnitType unit_type)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->resetUnitType(unit_type);
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+Unit const * ScalarUnit::getUnit() const
+{
+ g_assert(_unit_menu != nullptr);
+ return _unit_menu->getUnit();
+}
+
+UnitType ScalarUnit::getUnitType() const
+{
+ g_assert(_unit_menu);
+ return _unit_menu->getUnitType();
+}
+
+void ScalarUnit::setValue(double number, Glib::ustring const &units)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->setUnit(units);
+ Scalar::setValue(number);
+}
+
+void ScalarUnit::setValueKeepUnit(double number, Glib::ustring const &units)
+{
+ g_assert(_unit_menu != nullptr);
+ if (units == "") {
+ // set the value in the default units
+ Scalar::setValue(number);
+ } else {
+ double conversion = _unit_menu->getConversion(units);
+ Scalar::setValue(number / conversion);
+ }
+}
+
+void ScalarUnit::setValue(double number)
+{
+ Scalar::setValue(number);
+}
+
+double ScalarUnit::getValue(Glib::ustring const &unit_name) const
+{
+ g_assert(_unit_menu != nullptr);
+ if (unit_name == "") {
+ // Return the value in the default units
+ return Scalar::getValue();
+ } else {
+ double conversion = _unit_menu->getConversion(unit_name);
+ return conversion * Scalar::getValue();
+ }
+}
+
+void ScalarUnit::grabFocusAndSelectEntry()
+{
+ _widget->grab_focus();
+ static_cast<SpinButton*>(_widget)->select_region(0, 20);
+}
+
+
+void ScalarUnit::setHundredPercent(double number)
+{
+ _hundred_percent = number;
+}
+
+void ScalarUnit::setAbsoluteIsIncrement(bool value)
+{
+ _absolute_is_increment = value;
+}
+
+void ScalarUnit::setPercentageIsIncrement(bool value)
+{
+ _percentage_is_increment = value;
+}
+
+double ScalarUnit::PercentageToAbsolute(double value)
+{
+ // convert from percent to absolute
+ double convertedVal = 0;
+ double hundred_converted = _hundred_percent / _unit_menu->getConversion("px"); // _hundred_percent is in px
+ if (_percentage_is_increment)
+ value += 100;
+ convertedVal = 0.01 * hundred_converted * value;
+ if (_absolute_is_increment)
+ convertedVal -= hundred_converted;
+
+ return convertedVal;
+}
+
+double ScalarUnit::AbsoluteToPercentage(double value)
+{
+ double convertedVal = 0;
+ // convert from absolute to percent
+ if (_hundred_percent == 0) {
+ if (_percentage_is_increment)
+ convertedVal = 0;
+ else
+ convertedVal = 100;
+ } else {
+ double hundred_converted = _hundred_percent / _unit_menu->getConversion("px", lastUnits); // _hundred_percent is in px
+ if (_absolute_is_increment)
+ value += hundred_converted;
+ convertedVal = 100 * value / hundred_converted;
+ if (_percentage_is_increment)
+ convertedVal -= 100;
+ }
+
+ return convertedVal;
+}
+
+double ScalarUnit::getAsPercentage()
+{
+ double convertedVal = AbsoluteToPercentage(Scalar::getValue());
+ return convertedVal;
+}
+
+
+void ScalarUnit::setFromPercentage(double value)
+{
+ double absolute = PercentageToAbsolute(value);
+ Scalar::setValue(absolute);
+}
+
+
+void ScalarUnit::on_unit_changed()
+{
+ g_assert(_unit_menu != nullptr);
+
+ Glib::ustring abbr = _unit_menu->getUnitAbbr();
+ _suffix->set_label(abbr);
+
+ Inkscape::Util::Unit const *new_unit = unit_table.getUnit(abbr);
+ Inkscape::Util::Unit const *old_unit = unit_table.getUnit(lastUnits);
+
+ double convertedVal = 0;
+ if (old_unit->type == UNIT_TYPE_DIMENSIONLESS && new_unit->type == UNIT_TYPE_LINEAR) {
+ convertedVal = PercentageToAbsolute(Scalar::getValue());
+ } else if (old_unit->type == UNIT_TYPE_LINEAR && new_unit->type == UNIT_TYPE_DIMENSIONLESS) {
+ convertedVal = AbsoluteToPercentage(Scalar::getValue());
+ } else {
+ double conversion = _unit_menu->getConversion(lastUnits);
+ convertedVal = Scalar::getValue() / conversion;
+ }
+ Scalar::setValue(convertedVal);
+
+ lastUnits = abbr;
+}
+
+} // namespace Widget
+} // 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/widget/scalar-unit.h b/src/ui/widget/scalar-unit.h
new file mode 100644
index 0000000..e82c41d
--- /dev/null
+++ b/src/ui/widget/scalar-unit.h
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Derek P. Moore <derekm@hackunix.org>
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2004-2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SCALAR_UNIT_H
+#define INKSCAPE_UI_WIDGET_SCALAR_UNIT_H
+
+#include "scalar.h"
+#include "unit-menu.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional icon or suffix, for
+ * entering the values of various unit types.
+ *
+ * A ScalarUnit is a control for entering, viewing, or manipulating
+ * numbers with units. This differs from ordinary numbers like 2 or
+ * 3.14 because the number portion of a scalar *only* has meaning
+ * when considered with its unit type. For instance, 12 m and 12 in
+ * have very different actual values, but 1 m and 100 cm have the same
+ * value. The ScalarUnit allows us to abstract the presentation of
+ * the scalar to the user from the internal representations used by
+ * the program.
+ */
+class ScalarUnit : public Scalar
+{
+public:
+ /**
+ * Construct a ScalarUnit.
+ *
+ * @param label Label.
+ * @param unit_type Unit type (defaults to UNIT_TYPE_LINEAR).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param unit_menu UnitMenu drop down; if not specified, one will be created
+ * and displayed after the widget (defaults to NULL).
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ UnitType unit_type = UNIT_TYPE_LINEAR,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ UnitMenu *unit_menu = nullptr,
+ bool mnemonic = true);
+
+ /**
+ * Construct a ScalarUnit.
+ *
+ * @param label Label.
+ * @param tooltip Tooltip text.
+ * @param take_unitmenu Use the unitmenu from this parameter.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ ScalarUnit &take_unitmenu,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Initializes the scalar based on the settings in _unit_menu.
+ * Requires that _unit_menu has already been initialized.
+ */
+ void initScalar(double min_value, double max_value);
+
+ /**
+ * Gets the object for the currently selected unit.
+ */
+ Unit const * getUnit() const;
+
+ /**
+ * Gets the UnitType ID for the unit.
+ */
+ UnitType getUnitType() const;
+
+ /**
+ * Returns the value in the given unit system.
+ */
+ double getValue(Glib::ustring const &units) const;
+
+ /**
+ * Sets the unit for the ScalarUnit widget.
+ */
+ bool setUnit(Glib::ustring const &units);
+
+ /**
+ * Adds the unit type to the ScalarUnit widget.
+ */
+ void setUnitType(UnitType unit_type);
+
+ /**
+ * Resets the unit type for the ScalarUnit widget.
+ */
+ void resetUnitType(UnitType unit_type);
+
+ /**
+ * Sets the number and unit system.
+ */
+ void setValue(double number, Glib::ustring const &units);
+
+ /**
+ * Convert and sets the number only and keeps the current unit.
+ */
+ void setValueKeepUnit(double number, Glib::ustring const &units);
+
+ /**
+ * Sets the number only.
+ */
+ void setValue(double number);
+
+ /**
+ * Grab focus, and select the text that is in the entry field.
+ */
+ void grabFocusAndSelectEntry();
+
+ void setHundredPercent(double number);
+
+ void setAbsoluteIsIncrement(bool value);
+
+ void setPercentageIsIncrement(bool value);
+
+ /**
+ * Convert value from % to absolute, using _hundred_percent and *_is_increment flags.
+ */
+ double PercentageToAbsolute(double value);
+
+ /**
+ * Convert value from absolute to %, using _hundred_percent and *_is_increment flags.
+ */
+ double AbsoluteToPercentage(double value);
+
+ /**
+ * Assuming the current unit is absolute, get the corresponding % value.
+ */
+ double getAsPercentage();
+
+ /**
+ * Assuming the current unit is absolute, set the value corresponding to a given %.
+ */
+ void setFromPercentage(double value);
+
+ /**
+ * Signal handler for updating the value and suffix label when unit is changed.
+ */
+ void on_unit_changed();
+
+protected:
+ UnitMenu *_unit_menu;
+
+ double _hundred_percent; // the length that corresponds to 100%, in px, for %-to/from-absolute conversions
+
+ bool _absolute_is_increment; // if true, 120% with _hundred_percent=100px gets converted to/from 20px; otherwise, to/from 120px
+ bool _percentage_is_increment; // if true, 120px with _hundred_percent=100px gets converted to/from 20%; otherwise, to/from 120%
+ // if both are true, 20px is converted to/from 20% if _hundred_percent=100px
+
+ Glib::ustring lastUnits; // previously selected unit, for conversions
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SCALAR_UNIT_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/widget/scalar.cpp b/src/ui/widget/scalar.cpp
new file mode 100644
index 0000000..471de49
--- /dev/null
+++ b/src/ui/widget/scalar.cpp
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
+ *
+ * Copyright (C) 2004-2011 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scalar.h"
+#include "spinbutton.h"
+#include <gtkmm/scale.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(adjust, 0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+unsigned Scalar::getDigits() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_digits();
+}
+
+double Scalar::getStep() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return step;
+}
+
+double Scalar::getPage() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return page;
+}
+
+double Scalar::getRangeMin() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return min;
+}
+
+double Scalar::getRangeMax() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return max;
+}
+
+double Scalar::getValue() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value();
+}
+
+int Scalar::getValueAsInt() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value_as_int();
+}
+
+
+void Scalar::setDigits(unsigned digits)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_digits(digits);
+}
+
+void Scalar::setIncrements(double step, double /*page*/)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_increments(step, 0);
+}
+
+void Scalar::setRange(double min, double max)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_range(min, max);
+}
+
+void Scalar::setValue(double value, bool setProg)
+{
+ g_assert(_widget != nullptr);
+ if (setProg) {
+ setProgrammatically = true; // callback is supposed to reset back, if it cares
+ }
+ static_cast<SpinButton*>(_widget)->set_value(value);
+}
+
+void Scalar::setWidthChars(unsigned chars)
+{
+ g_assert(_widget != NULL);
+ static_cast<SpinButton*>(_widget)->set_width_chars(chars);
+}
+
+void Scalar::update()
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->update();
+}
+
+void Scalar::addSlider()
+{
+ auto scale = new Gtk::Scale(static_cast<SpinButton*>(_widget)->get_adjustment());
+ scale->set_draw_value(false);
+ add (*manage (scale));
+}
+
+Glib::SignalProxy0<void> Scalar::signal_value_changed()
+{
+ return static_cast<SpinButton*>(_widget)->signal_value_changed();
+}
+
+Glib::SignalProxy1<bool, GdkEventButton*> Scalar::signal_button_release_event()
+{
+ return static_cast<SpinButton*>(_widget)->signal_button_release_event();
+}
+
+
+} // namespace Widget
+} // 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/widget/scalar.h b/src/ui/widget/scalar.h
new file mode 100644
index 0000000..29a14d1
--- /dev/null
+++ b/src/ui/widget/scalar.h
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SCALAR_H
+#define INKSCAPE_UI_WIDGET_SCALAR_H
+
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values.
+ */
+class Scalar : public Labelled
+{
+public:
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Fetches the precision of the spin button.
+ */
+ unsigned getDigits() const;
+
+ /**
+ * Gets the current step increment used by the spin button.
+ */
+ double getStep() const;
+
+ /**
+ * Gets the current page increment used by the spin button.
+ */
+ double getPage() const;
+
+ /**
+ * Gets the minimum range value allowed for the spin button.
+ */
+ double getRangeMin() const;
+
+ /**
+ * Gets the maximum range value allowed for the spin button.
+ */
+ double getRangeMax() const;
+
+ bool getSnapToTicks() const;
+
+ /**
+ * Get the value in the spin_button.
+ */
+ double getValue() const;
+
+ /**
+ * Get the value spin_button represented as an integer.
+ */
+ int getValueAsInt() const;
+
+ /**
+ * Sets the precision to be displayed by the spin button.
+ */
+ void setDigits(unsigned digits);
+
+ /**
+ * Sets the step and page increments for the spin button.
+ * @todo Remove the second parameter - deprecated
+ */
+ void setIncrements(double step, double page);
+
+ /**
+ * Sets the minimum and maximum range allowed for the spin button.
+ */
+ void setRange(double min, double max);
+
+ /**
+ * Sets the value of the spin button.
+ */
+ void setValue(double value, bool setProg = true);
+
+ /**
+ * Sets the width of the spin button by number of characters.
+ */
+ void setWidthChars(unsigned chars);
+
+ /**
+ * Manually forces an update of the spin button.
+ */
+ void update();
+
+ /**
+ * Adds a slider (HScale) to the left of the spinbox.
+ */
+ void addSlider();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_value_changed();
+
+ /**
+ * Signal raised when the spin button's pressed.
+ */
+ Glib::SignalProxy1<bool, GdkEventButton*> signal_button_release_event();
+
+ /**
+ * true if the value was set by setValue, not changed by the user;
+ * if a callback checks it, it must reset it back to false.
+ */
+ bool setProgrammatically;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SCALAR_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/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp
new file mode 100644
index 0000000..9417a9f
--- /dev/null
+++ b/src/ui/widget/selected-style.cpp
@@ -0,0 +1,1488 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * buliabyak@gmail.com
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2005 author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "selected-style.h"
+
+#include <gtkmm/separatormenuitem.h>
+
+
+#include "desktop-style.h"
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "message-context.h"
+#include "selection.h"
+#include "sp-cursor.h"
+
+#include "display/sp-canvas.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/sp-hatch.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-namedview.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "style.h"
+
+#include "ui/pixmaps/cursor-adj-a.xpm"
+#include "ui/pixmaps/cursor-adj-h.xpm"
+#include "ui/pixmaps/cursor-adj-l.xpm"
+#include "ui/pixmaps/cursor-adj-s.xpm"
+
+#include "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/dialog/panel-dialog.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/color-preview.h"
+
+#include "widgets/ege-paint-def.h"
+#include "widgets/gradient-image.h"
+#include "widgets/spinbutton-events.h"
+#include "widgets/spw-utilities.h"
+#include "widgets/widget-sizes.h"
+
+using Inkscape::Util::unit_table;
+
+static gdouble const _sw_presets[] = { 32 , 16 , 10 , 8 , 6 , 4 , 3 , 2 , 1.5 , 1 , 0.75 , 0.5 , 0.25 , 0.1 };
+static gchar const *const _sw_presets_str[] = {"32", "16", "10", "8", "6", "4", "3", "2", "1.5", "1", "0.75", "0.5", "0.25", "0.1"};
+
+static void
+ss_selection_changed (Inkscape::Selection *, gpointer data)
+{
+ Inkscape::UI::Widget::SelectedStyle *ss = (Inkscape::UI::Widget::SelectedStyle *) data;
+ ss->update();
+}
+
+static void
+ss_selection_modified( Inkscape::Selection *selection, guint flags, gpointer data )
+{
+ // Don't update the style when dragging or doing non-style related changes
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ ss_selection_changed (selection, data);
+ }
+}
+
+static void
+ss_subselection_changed( gpointer /*dragger*/, gpointer data )
+{
+ ss_selection_changed (nullptr, data);
+}
+
+namespace {
+
+void clearTooltip( Gtk::Widget &widget )
+{
+ widget.set_tooltip_text("");
+ widget.set_has_tooltip(false);
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+struct DropTracker {
+ SelectedStyle* parent;
+ int item;
+};
+
+/* Drag and Drop */
+enum ui_drop_target_info {
+ APP_OSWB_COLOR
+};
+
+//TODO: warning: deprecated conversion from string constant to ‘gchar*’
+//
+//Turn out to be warnings that we should probably leave in place. The
+// pointers/types used need to be read-only. So until we correct the using
+// code, those warnings are actually desired. They say "Hey! Fix this". We
+// definitely don't want to hide/ignore them. --JonCruz
+static const GtkTargetEntry ui_drop_target_entries [] = {
+ {g_strdup("application/x-oswb-color"), 0, APP_OSWB_COLOR}
+};
+
+static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries);
+
+/* convenience function */
+static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop);
+
+SelectedStyle::SelectedStyle(bool /*layout*/)
+ : current_stroke_width(0)
+ , _sw_unit(nullptr)
+ , _desktop(nullptr)
+ , _table()
+ , _fill_label(_("Fill:"))
+ , _stroke_label(_("Stroke:"))
+ , _opacity_label(_("O:"))
+ , _fill_place(this, SS_FILL)
+ , _stroke_place(this, SS_STROKE)
+ , _fill_flag_place()
+ , _stroke_flag_place()
+ , _opacity_place()
+ , _opacity_adjustment(Gtk::Adjustment::create(100, 0.0, 100, 1.0, 10.0))
+ , _opacity_sb(0.02, 0)
+ , _fill()
+ , _stroke()
+ , _stroke_width_place(this)
+ , _stroke_width("")
+ , _fill_empty_space("")
+ , _opacity_blocked(false)
+{
+ set_name("SelectedStyle");
+ _drop[0] = _drop[1] = nullptr;
+ _dropEnabled[0] = _dropEnabled[1] = false;
+
+ _fill_label.set_halign(Gtk::ALIGN_START);
+ _fill_label.set_valign(Gtk::ALIGN_CENTER);
+ _fill_label.set_margin_top(0);
+ _fill_label.set_margin_bottom(0);
+ _stroke_label.set_halign(Gtk::ALIGN_START);
+ _stroke_label.set_valign(Gtk::ALIGN_CENTER);
+ _stroke_label.set_margin_top(0);
+ _stroke_label.set_margin_bottom(0);
+ _opacity_label.set_halign(Gtk::ALIGN_START);
+ _opacity_label.set_valign(Gtk::ALIGN_CENTER);
+ _opacity_label.set_margin_top(0);
+ _opacity_label.set_margin_bottom(0);
+ _stroke_width.set_name("monoStrokeWidth");
+ _fill_empty_space.set_name("fillEmptySpace");
+
+ _fill_label.set_margin_start(0);
+ _fill_label.set_margin_end(0);
+ _stroke_label.set_margin_start(0);
+ _stroke_label.set_margin_end(0);
+ _opacity_label.set_margin_start(0);
+ _opacity_label.set_margin_end(0);
+
+ _table.set_column_spacing(2);
+ _table.set_row_spacing(0);
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+
+ _na[i].set_markup (_("N/A"));
+ _na[i].show_all();
+ __na[i] = (_("Nothing selected"));
+
+ if (i == SS_FILL) {
+ _none[i].set_markup (C_("Fill", "<i>None</i>"));
+ } else {
+ _none[i].set_markup (C_("Stroke", "<i>None</i>"));
+ }
+ _none[i].show_all();
+ __none[i] = (i == SS_FILL)? (C_("Fill and stroke", "No fill, middle-click for black fill")) : (C_("Fill and stroke", "No stroke, middle-click for black stroke"));
+
+ _pattern[i].set_markup (_("Pattern"));
+ _pattern[i].show_all();
+ __pattern[i] = (i == SS_FILL)? (_("Pattern fill")) : (_("Pattern stroke"));
+
+ _hatch[i].set_markup(_("Hatch"));
+ _hatch[i].show_all();
+ __hatch[i] = (i == SS_FILL) ? (_("Hatch fill")) : (_("Hatch stroke"));
+
+ _lgradient[i].set_markup (_("<b>L</b>"));
+ _lgradient[i].show_all();
+ __lgradient[i] = (i == SS_FILL)? (_("Linear gradient fill")) : (_("Linear gradient stroke"));
+
+ _gradient_preview_l[i] = GTK_WIDGET(sp_gradient_image_new (nullptr));
+ _gradient_box_l[i].pack_start(_lgradient[i]);
+ _gradient_box_l[i].pack_start(*(Glib::wrap(_gradient_preview_l[i])));
+ _gradient_box_l[i].show_all();
+
+ _rgradient[i].set_markup (_("<b>R</b>"));
+ _rgradient[i].show_all();
+ __rgradient[i] = (i == SS_FILL)? (_("Radial gradient fill")) : (_("Radial gradient stroke"));
+
+ _gradient_preview_r[i] = GTK_WIDGET(sp_gradient_image_new (nullptr));
+ _gradient_box_r[i].pack_start(_rgradient[i]);
+ _gradient_box_r[i].pack_start(*(Glib::wrap(_gradient_preview_r[i])));
+ _gradient_box_r[i].show_all();
+
+#ifdef WITH_MESH
+ _mgradient[i].set_markup (_("<b>M</b>"));
+ _mgradient[i].show_all();
+ __mgradient[i] = (i == SS_FILL)? (_("Mesh gradient fill")) : (_("Mesh gradient stroke"));
+
+ _gradient_preview_m[i] = GTK_WIDGET(sp_gradient_image_new (nullptr));
+ _gradient_box_m[i].pack_start(_mgradient[i]);
+ _gradient_box_m[i].pack_start(*(Glib::wrap(_gradient_preview_m[i])));
+ _gradient_box_m[i].show_all();
+#endif
+
+ _many[i].set_markup (_("Different"));
+ _many[i].show_all();
+ __many[i] = (i == SS_FILL)? (_("Different fills")) : (_("Different strokes"));
+
+ _unset[i].set_markup (_("<b>Unset</b>"));
+ _unset[i].show_all();
+ __unset[i] = (i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke"));
+
+ _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0);
+ __color[i] = (i == SS_FILL)? (_("Flat color fill")) : (_("Flat color stroke"));
+
+ // TRANSLATORS: A means "Averaged"
+ _averaged[i].set_markup (_("<b>a</b>"));
+ _averaged[i].show_all();
+ __averaged[i] = (i == SS_FILL)? (_("Fill is averaged over selected objects")) : (_("Stroke is averaged over selected objects"));
+
+ // TRANSLATORS: M means "Multiple"
+ _multiple[i].set_markup (_("<b>m</b>"));
+ _multiple[i].show_all();
+ __multiple[i] = (i == SS_FILL)? (_("Multiple selected objects have the same fill")) : (_("Multiple selected objects have the same stroke"));
+
+ _popup_edit[i].add(*(new Gtk::Label((i == SS_FILL)? _("Edit fill...") : _("Edit stroke..."), Gtk::ALIGN_START)));
+ _popup_edit[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_edit : &SelectedStyle::on_stroke_edit ));
+
+ _popup_lastused[i].add(*(new Gtk::Label(_("Last set color"), Gtk::ALIGN_START)));
+ _popup_lastused[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_lastused : &SelectedStyle::on_stroke_lastused ));
+
+ _popup_lastselected[i].add(*(new Gtk::Label(_("Last selected color"), Gtk::ALIGN_START)));
+ _popup_lastselected[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_lastselected : &SelectedStyle::on_stroke_lastselected ));
+
+ _popup_invert[i].add(*(new Gtk::Label(_("Invert"), Gtk::ALIGN_START)));
+ _popup_invert[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_invert : &SelectedStyle::on_stroke_invert ));
+
+ _popup_white[i].add(*(new Gtk::Label(_("White"), Gtk::ALIGN_START)));
+ _popup_white[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_white : &SelectedStyle::on_stroke_white ));
+
+ _popup_black[i].add(*(new Gtk::Label(_("Black"), Gtk::ALIGN_START)));
+ _popup_black[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_black : &SelectedStyle::on_stroke_black ));
+
+ _popup_copy[i].add(*(new Gtk::Label(_("Copy color"), Gtk::ALIGN_START)));
+ _popup_copy[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_copy : &SelectedStyle::on_stroke_copy ));
+
+ _popup_paste[i].add(*(new Gtk::Label(_("Paste color"), Gtk::ALIGN_START)));
+ _popup_paste[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_paste : &SelectedStyle::on_stroke_paste ));
+
+ _popup_swap[i].add(*(new Gtk::Label(_("Swap fill and stroke"), Gtk::ALIGN_START)));
+ _popup_swap[i].signal_activate().connect(sigc::mem_fun(*this,
+ &SelectedStyle::on_fillstroke_swap));
+
+ _popup_opaque[i].add(*(new Gtk::Label((i == SS_FILL)? _("Make fill opaque") : _("Make stroke opaque"), Gtk::ALIGN_START)));
+ _popup_opaque[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_opaque : &SelectedStyle::on_stroke_opaque ));
+
+ //TRANSLATORS COMMENT: unset is a verb here
+ _popup_unset[i].add(*(new Gtk::Label((i == SS_FILL)? _("Unset fill") : _("Unset stroke"), Gtk::ALIGN_START)));
+ _popup_unset[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_unset : &SelectedStyle::on_stroke_unset ));
+
+ _popup_remove[i].add(*(new Gtk::Label((i == SS_FILL)? _("Remove fill") : _("Remove stroke"), Gtk::ALIGN_START)));
+ _popup_remove[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_remove : &SelectedStyle::on_stroke_remove ));
+
+ _popup[i].attach(_popup_edit[i], 0,1, 0,1);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 1,2);
+ _popup[i].attach(_popup_lastused[i], 0,1, 2,3);
+ _popup[i].attach(_popup_lastselected[i], 0,1, 3,4);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 4,5);
+ _popup[i].attach(_popup_invert[i], 0,1, 5,6);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 6,7);
+ _popup[i].attach(_popup_white[i], 0,1, 7,8);
+ _popup[i].attach(_popup_black[i], 0,1, 8,9);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 9,10);
+ _popup[i].attach(_popup_copy[i], 0,1, 10,11);
+ _popup_copy[i].set_sensitive(false);
+ _popup[i].attach(_popup_paste[i], 0,1, 11,12);
+ _popup[i].attach(_popup_swap[i], 0,1, 12,13);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 13,14);
+ _popup[i].attach(_popup_opaque[i], 0,1, 14,15);
+ _popup[i].attach(_popup_unset[i], 0,1, 15,16);
+ _popup[i].attach(_popup_remove[i], 0,1, 16,17);
+ _popup[i].show_all();
+
+ _mode[i] = SS_NA;
+ }
+
+ {
+ int row = 0;
+
+ Inkscape::Util::UnitTable::UnitMap m = unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR);
+ Inkscape::Util::UnitTable::UnitMap::iterator iter = m.begin();
+ while(iter != m.end()) {
+ Gtk::RadioMenuItem *mi = Gtk::manage(new Gtk::RadioMenuItem(_sw_group));
+ mi->add(*(new Gtk::Label(iter->first, Gtk::ALIGN_START)));
+ _unit_mis.push_back(mi);
+ Inkscape::Util::Unit const *u = unit_table.getUnit(iter->first);
+ mi->signal_activate().connect(sigc::bind<Inkscape::Util::Unit const *>(sigc::mem_fun(*this, &SelectedStyle::on_popup_units), u));
+ _popup_sw.attach(*mi, 0,1, row, row+1);
+ row++;
+ ++iter;
+ }
+
+ _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1);
+ row++;
+
+ for (guint i = 0; i < G_N_ELEMENTS(_sw_presets_str); ++i) {
+ Gtk::MenuItem *mi = Gtk::manage(new Gtk::MenuItem());
+ mi->add(*(new Gtk::Label(_sw_presets_str[i], Gtk::ALIGN_START)));
+ mi->signal_activate().connect(sigc::bind<int>(sigc::mem_fun(*this, &SelectedStyle::on_popup_preset), i));
+ _popup_sw.attach(*mi, 0,1, row, row+1);
+ row++;
+ }
+
+ _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1);
+ row++;
+
+ _popup_sw_remove.add(*(new Gtk::Label(_("Remove"), Gtk::ALIGN_START)));
+ _popup_sw_remove.signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_remove));
+ _popup_sw.attach(_popup_sw_remove, 0,1, row, row+1);
+ row++;
+
+ _popup_sw.show_all();
+ }
+
+ _fill_place.add(_na[SS_FILL]);
+ _fill_place.set_tooltip_text(__na[SS_FILL]);
+ _fill.pack_start(_fill_place, Gtk::PACK_SHRINK);
+ _fill.pack_start(_fill_empty_space, Gtk::PACK_SHRINK);
+
+ _stroke_place.add(_na[SS_STROKE]);
+ _stroke_place.set_tooltip_text(__na[SS_STROKE]);
+
+ _stroke.pack_start(_stroke_place);
+ _stroke_width_place.add(_stroke_width);
+ _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK);
+
+ _opacity_sb.set_adjustment(_opacity_adjustment);
+ _opacity_sb.set_size_request (SELECTED_STYLE_SB_WIDTH, -1);
+ _opacity_sb.set_sensitive (false);
+
+ _table.attach(_fill_label, 0, 0, 1, 1);
+ _table.attach(_stroke_label, 0, 1, 1, 1);
+
+ _table.attach(_fill_flag_place, 1, 0, 1, 1);
+ _table.attach(_stroke_flag_place, 1, 1, 1, 1);
+
+ _table.attach(_fill, 2, 0, 1, 1);
+ _table.attach(_stroke, 2, 1, 1, 1);
+
+ _opacity_place.add(_opacity_label);
+
+ _table.attach(_opacity_place, 4, 0, 1, 2);
+ _table.attach(_opacity_sb, 5, 0, 1, 2);
+
+ pack_start(_table, true, true, 2);
+
+ set_size_request (SELECTED_STYLE_WIDTH, -1);
+
+ _drop[SS_FILL] = new DropTracker();
+ ((DropTracker*)_drop[SS_FILL])->parent = this;
+ ((DropTracker*)_drop[SS_FILL])->item = SS_FILL;
+
+ _drop[SS_STROKE] = new DropTracker();
+ ((DropTracker*)_drop[SS_STROKE])->parent = this;
+ ((DropTracker*)_drop[SS_STROKE])->item = SS_STROKE;
+
+ g_signal_connect(_stroke_place.gobj(),
+ "drag_data_received",
+ G_CALLBACK(dragDataReceived),
+ _drop[SS_STROKE]);
+
+ g_signal_connect(_fill_place.gobj(),
+ "drag_data_received",
+ G_CALLBACK(dragDataReceived),
+ _drop[SS_FILL]);
+
+ _fill_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_fill_click));
+ _stroke_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_click));
+ _opacity_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_click));
+ _stroke_width_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click));
+ _stroke_width_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click));
+ _opacity_sb.signal_populate_popup().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_menu));
+ _opacity_sb.signal_value_changed().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_changed));
+ // Connect to key-press to ensure focus is consistent with other spin buttons when using the keys vs mouse-click
+ g_signal_connect (G_OBJECT (_opacity_sb.gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), _opacity_sb.gobj());
+ g_signal_connect (G_OBJECT (_opacity_sb.gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), _opacity_sb.gobj());
+}
+
+SelectedStyle::~SelectedStyle()
+{
+ selection_changed_connection->disconnect();
+ delete selection_changed_connection;
+ selection_modified_connection->disconnect();
+ delete selection_modified_connection;
+ subselection_changed_connection->disconnect();
+ delete subselection_changed_connection;
+ _unit_mis.clear();
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ delete _color_preview[i];
+ // FIXME: do we need this? the destroy methods are not exported
+ //sp_gradient_image_destroy(GTK_OBJECT(_gradient_preview_l[i]));
+ //sp_gradient_image_destroy(GTK_OBJECT(_gradient_preview_r[i]));
+ }
+
+ delete (DropTracker*)_drop[SS_FILL];
+ delete (DropTracker*)_drop[SS_STROKE];
+}
+
+void
+SelectedStyle::setDesktop(SPDesktop *desktop)
+{
+ _desktop = desktop;
+ g_object_set_data (G_OBJECT(_opacity_sb.gobj()), "dtw", _desktop->canvas);
+
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ selection_changed_connection = new sigc::connection (selection->connectChanged(
+ sigc::bind (
+ sigc::ptr_fun(&ss_selection_changed),
+ this )
+ ));
+ selection_modified_connection = new sigc::connection (selection->connectModified(
+ sigc::bind (
+ sigc::ptr_fun(&ss_selection_modified),
+ this )
+ ));
+ subselection_changed_connection = new sigc::connection (desktop->connectToolSubselectionChanged(
+ sigc::bind (
+ sigc::ptr_fun(&ss_subselection_changed),
+ this )
+ ));
+
+ _sw_unit = desktop->getNamedView()->display_units;
+
+ // Set the doc default unit active in the units list
+ for ( auto mi:_unit_mis ) {
+ if (mi && mi->get_label() == _sw_unit->abbr) {
+ mi->set_active();
+ break;
+ }
+ }
+}
+
+void SelectedStyle::dragDataReceived( GtkWidget */*widget*/,
+ GdkDragContext */*drag_context*/,
+ gint /*x*/, gint /*y*/,
+ GtkSelectionData *data,
+ guint /*info*/,
+ guint /*event_time*/,
+ gpointer user_data )
+{
+ DropTracker* tracker = (DropTracker*)user_data;
+
+ // copied from drag-and-drop.cpp, case APP_OSWB_COLOR
+ bool worked = false;
+ Glib::ustring colorspec;
+ if (gtk_selection_data_get_format(data) == 8) {
+ ege::PaintDef color;
+ worked = color.fromMIMEData("application/x-oswb-color",
+ reinterpret_cast<char const *>(gtk_selection_data_get_data(data)),
+ gtk_selection_data_get_length(data),
+ gtk_selection_data_get_format(data));
+ if (worked) {
+ if (color.getType() == ege::PaintDef::CLEAR) {
+ colorspec = ""; // TODO check if this is sufficient
+ } else if (color.getType() == ege::PaintDef::NONE) {
+ colorspec = "none";
+ } else {
+ unsigned int r = color.getR();
+ unsigned int g = color.getG();
+ unsigned int b = color.getB();
+
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b);
+ colorspec = tmp;
+ g_free(tmp);
+ }
+ }
+ }
+ if (worked) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, (tracker->item == SS_FILL) ? "fill":"stroke", colorspec.c_str());
+
+ sp_desktop_set_style(tracker->parent->_desktop, css);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(tracker->parent->_desktop->getDocument(), SP_VERB_NONE, _("Drop color"));
+ }
+}
+
+void SelectedStyle::on_fill_remove() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill", "none");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Remove fill"));
+}
+
+void SelectedStyle::on_stroke_remove() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke", "none");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Remove stroke"));
+}
+
+void SelectedStyle::on_fill_unset() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_unset_property (css, "fill");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Unset fill"));
+}
+
+void SelectedStyle::on_stroke_unset() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_unset_property (css, "stroke");
+ sp_repr_css_unset_property (css, "stroke-opacity");
+ sp_repr_css_unset_property (css, "stroke-width");
+ sp_repr_css_unset_property (css, "stroke-miterlimit");
+ sp_repr_css_unset_property (css, "stroke-linejoin");
+ sp_repr_css_unset_property (css, "stroke-linecap");
+ sp_repr_css_unset_property (css, "stroke-dashoffset");
+ sp_repr_css_unset_property (css, "stroke-dasharray");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Unset stroke"));
+}
+
+void SelectedStyle::on_fill_opaque() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Make fill opaque"));
+}
+
+void SelectedStyle::on_stroke_opaque() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke-opacity", "1");
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Make fill opaque"));
+}
+
+void SelectedStyle::on_fill_lastused() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = sp_desktop_get_color(_desktop, true);
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), color);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Apply last set color to fill"));
+}
+
+void SelectedStyle::on_stroke_lastused() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = sp_desktop_get_color(_desktop, false);
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), color);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Apply last set color to stroke"));
+}
+
+void SelectedStyle::on_fill_lastselected() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _lastselected[SS_FILL]);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Apply last selected color to fill"));
+}
+
+void SelectedStyle::on_stroke_lastselected() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _lastselected[SS_STROKE]);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Apply last selected color to stroke"));
+}
+
+void SelectedStyle::on_fill_invert() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = _thisselected[SS_FILL];
+ gchar c[64];
+ if (_mode[SS_FILL] == SS_LGRADIENT || _mode[SS_FILL] == SS_RGRADIENT) {
+ sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_FILL);
+ return;
+
+ }
+
+ if (_mode[SS_FILL] != SS_COLOR) return;
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(color)),
+ (255 - SP_RGBA32_G_U(color)),
+ (255 - SP_RGBA32_B_U(color)),
+ SP_RGBA32_A_U(color)
+ )
+ );
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Invert fill"));
+}
+
+void SelectedStyle::on_stroke_invert() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = _thisselected[SS_STROKE];
+ gchar c[64];
+ if (_mode[SS_STROKE] == SS_LGRADIENT || _mode[SS_STROKE] == SS_RGRADIENT) {
+ sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_STROKE);
+ return;
+ }
+ if (_mode[SS_STROKE] != SS_COLOR) return;
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(color)),
+ (255 - SP_RGBA32_G_U(color)),
+ (255 - SP_RGBA32_B_U(color)),
+ SP_RGBA32_A_U(color)
+ )
+ );
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Invert stroke"));
+}
+
+void SelectedStyle::on_fill_white() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0xffffffff);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("White fill"));
+}
+
+void SelectedStyle::on_stroke_white() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0xffffffff);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_repr_css_set_property (css, "stroke-opacity", "1");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("White stroke"));
+}
+
+void SelectedStyle::on_fill_black() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0x000000ff);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_repr_css_set_property (css, "fill-opacity", "1.0");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Black fill"));
+}
+
+void SelectedStyle::on_stroke_black() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0x000000ff);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_repr_css_set_property (css, "stroke-opacity", "1.0");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Black stroke"));
+}
+
+void SelectedStyle::on_fill_copy() {
+ if (_mode[SS_FILL] == SS_COLOR) {
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _thisselected[SS_FILL]);
+ Glib::ustring text;
+ text += c;
+ if (!text.empty()) {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ refClipboard->set_text(text);
+ }
+ }
+}
+
+void SelectedStyle::on_stroke_copy() {
+ if (_mode[SS_STROKE] == SS_COLOR) {
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _thisselected[SS_STROKE]);
+ Glib::ustring text;
+ text += c;
+ if (!text.empty()) {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ refClipboard->set_text(text);
+ }
+ }
+}
+
+void SelectedStyle::on_fill_paste() {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ Glib::ustring const text = refClipboard->wait_for_text();
+
+ if (!text.empty()) {
+ guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity
+ if (color == 0x000000ff) // failed to parse color string
+ return;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill", text.c_str());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Paste fill"));
+ }
+}
+
+void SelectedStyle::on_stroke_paste() {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ Glib::ustring const text = refClipboard->wait_for_text();
+
+ if (!text.empty()) {
+ guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity
+ if (color == 0x000000ff) // failed to parse color string
+ return;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke", text.c_str());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Paste stroke"));
+ }
+}
+
+void SelectedStyle::on_fillstroke_swap() {
+ _desktop->getSelection()->swapFillStroke();
+}
+
+void SelectedStyle::on_fill_edit() {
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageFill();
+}
+
+void SelectedStyle::on_stroke_edit() {
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokePaint();
+}
+
+bool
+SelectedStyle::on_fill_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageFill();
+
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup[SS_FILL].popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastcolor
+ if (_mode[SS_FILL] == SS_NONE) {
+ on_fill_lastused();
+ } else {
+ on_fill_remove();
+ }
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_stroke_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokePaint();
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup[SS_STROKE].popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastcolor
+ if (_mode[SS_STROKE] == SS_NONE) {
+ on_stroke_lastused();
+ } else {
+ on_stroke_remove();
+ }
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_sw_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokeStyle();
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup_sw.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastwidth?
+ //
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_opacity_click(GdkEventButton *event)
+{
+ if (event->button == 2) { // middle click
+ const char* opacity = _opacity_sb.get_value() < 50? "0.5" : (_opacity_sb.get_value() == 100? "0" : "1");
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "opacity", opacity);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Change opacity"));
+ return true;
+ }
+
+ return false;
+}
+
+void SelectedStyle::on_popup_units(Inkscape::Util::Unit const *unit) {
+ _sw_unit = unit;
+ update();
+}
+
+void SelectedStyle::on_popup_preset(int i) {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gdouble w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(_sw_presets[i], _sw_unit, "px");
+ } else {
+ w = _sw_presets[i];
+ }
+ Inkscape::CSSOStringStream os;
+ os << w;
+ sp_repr_css_set_property (css, "stroke-width", os.str().c_str());
+ // FIXME: update dash patterns!
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_SWATCHES,
+ _("Change stroke width"));
+}
+
+void
+SelectedStyle::update()
+{
+ if (_desktop == nullptr)
+ return;
+
+ // create temporary style
+ SPStyle query(_desktop->getDocument());
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ Gtk::EventBox *place = (i == SS_FILL)? &_fill_place : &_stroke_place;
+ Gtk::EventBox *flag_place = (i == SS_FILL)? &_fill_flag_place : &_stroke_flag_place;
+
+ place->remove();
+ flag_place->remove();
+
+ clearTooltip(*place);
+ clearTooltip(*flag_place);
+
+ _mode[i] = SS_NA;
+ _paintserver_id[i].clear();
+
+ _popup_copy[i].set_sensitive(false);
+
+ // query style from desktop. This returns a result flag and fills query with the style of subselection, if any, or selection
+ int result = sp_desktop_query_style (_desktop, &query,
+ (i == SS_FILL)? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE);
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ place->add(_na[i]);
+ place->set_tooltip_text(__na[i]);
+ _mode[i] = SS_NA;
+ if ( _dropEnabled[i] ) {
+ gtk_drag_dest_unset( GTK_WIDGET((i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()) );
+ _dropEnabled[i] = false;
+ }
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ if ( !_dropEnabled[i] ) {
+ gtk_drag_dest_set( GTK_WIDGET( (i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()),
+ GTK_DEST_DEFAULT_ALL,
+ ui_drop_target_entries,
+ nui_drop_target_entries,
+ GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) );
+ _dropEnabled[i] = true;
+ }
+ SPIPaint *paint;
+ if (i == SS_FILL) {
+ paint = &(query.fill);
+ } else {
+ paint = &(query.stroke);
+ }
+ if (paint->set && paint->isPaintserver()) {
+ SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query);
+ if ( server ) {
+ Inkscape::XML::Node *srepr = server->getRepr();
+ _paintserver_id[i] += "url(#";
+ _paintserver_id[i] += srepr->attribute("id");
+ _paintserver_id[i] += ")";
+
+ if (SP_IS_LINEARGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_l[i]), vector);
+ place->add(_gradient_box_l[i]);
+ place->set_tooltip_text(__lgradient[i]);
+ _mode[i] = SS_LGRADIENT;
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_r[i]), vector);
+ place->add(_gradient_box_r[i]);
+ place->set_tooltip_text(__rgradient[i]);
+ _mode[i] = SS_RGRADIENT;
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ SPGradient *array = SP_GRADIENT(server)->getArray();
+ sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_m[i]), array);
+ place->add(_gradient_box_m[i]);
+ place->set_tooltip_text(__mgradient[i]);
+ _mode[i] = SS_MGRADIENT;
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ place->add(_pattern[i]);
+ place->set_tooltip_text(__pattern[i]);
+ _mode[i] = SS_PATTERN;
+ } else if (SP_IS_HATCH(server)) {
+ place->add(_hatch[i]);
+ place->set_tooltip_text(__hatch[i]);
+ _mode[i] = SS_HATCH;
+ }
+ } else {
+ g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__);
+ }
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32(
+ SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query.fill_opacity.value : query.stroke_opacity.value));
+ _lastselected[i] = _thisselected[i];
+ _thisselected[i] = color; // include opacity
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar c_string[64];
+ g_snprintf (c_string, 64, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color));
+ place->set_tooltip_text(__color[i] + ": " + c_string + _(", drag to adjust, middle-click to remove"));
+ _mode[i] = SS_COLOR;
+ _popup_copy[i].set_sensitive(true);
+
+ } else if (paint->set && paint->isNone()) {
+ place->add(_none[i]);
+ place->set_tooltip_text(__none[i]);
+ _mode[i] = SS_NONE;
+ } else if (!paint->set) {
+ place->add(_unset[i]);
+ place->set_tooltip_text(__unset[i]);
+ _mode[i] = SS_UNSET;
+ }
+ if (result == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ flag_place->add(_averaged[i]);
+ flag_place->set_tooltip_text(__averaged[i]);
+ } else if (result == QUERY_STYLE_MULTIPLE_SAME) {
+ flag_place->add(_multiple[i]);
+ flag_place->set_tooltip_text(__multiple[i]);
+ }
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ place->add(_many[i]);
+ place->set_tooltip_text(__many[i]);
+ _mode[i] = SS_MANY;
+ break;
+ default:
+ break;
+ }
+ }
+
+// Now query opacity
+ clearTooltip(_opacity_place);
+ clearTooltip(_opacity_sb);
+
+ int result = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_MASTEROPACITY);
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ _opacity_place.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_sensitive(false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _opacity_place.set_tooltip_text(_("Opacity (%)"));
+ _opacity_sb.set_tooltip_text(_("Opacity (%)"));
+ if (_opacity_blocked) break;
+ _opacity_blocked = true;
+ _opacity_sb.set_sensitive(true);
+ _opacity_adjustment->set_value(SP_SCALE24_TO_FLOAT(query.opacity.value) * 100);
+ _opacity_blocked = false;
+ break;
+ }
+
+// Now query stroke_width
+ int result_sw = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ switch (result_sw) {
+ case QUERY_STYLE_NOTHING:
+ _stroke_width.set_markup("");
+ current_stroke_width = 0;
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ double w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", _sw_unit);
+ } else {
+ w = query.stroke_width.computed;
+ }
+ current_stroke_width = w;
+
+ {
+ gchar *str = g_strdup_printf(" %#.3g", w);
+ if (str[strlen(str) - 1] == ',' || str[strlen(str) - 1] == '.') {
+ str[strlen(str)-1] = '\0';
+ }
+ _stroke_width.set_markup(str);
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Stroke width: %.5g%s%s"),
+ w,
+ _sw_unit? _sw_unit->abbr.c_str() : "px",
+ (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED)?
+ _(" (averaged)") : "");
+ _stroke_width_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void SelectedStyle::opacity_0() {_opacity_sb.set_value(0);}
+void SelectedStyle::opacity_025() {_opacity_sb.set_value(25);}
+void SelectedStyle::opacity_05() {_opacity_sb.set_value(50);}
+void SelectedStyle::opacity_075() {_opacity_sb.set_value(75);}
+void SelectedStyle::opacity_1() {_opacity_sb.set_value(100);}
+
+void SelectedStyle::on_opacity_menu (Gtk::Menu *menu) {
+
+ Glib::ListHandle<Gtk::Widget *> children = menu->get_children();
+ for (auto iter : children) {
+ menu->remove(*iter);
+ }
+
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label(_("0 (transparent)"), Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_0 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("25%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_025 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("50%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_05 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("75%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_075 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label(_("100% (opaque)"), Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_1 ));
+ menu->add(*item);
+ }
+
+ menu->show_all();
+}
+
+void SelectedStyle::on_opacity_changed ()
+{
+ g_return_if_fail(_desktop); // TODO this shouldn't happen!
+ if (_opacity_blocked)
+ return;
+ _opacity_blocked = true;
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream os;
+ os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0);
+ sp_repr_css_set_property (css, "opacity", os.str().c_str());
+ // FIXME: workaround for GTK breakage: display interruptibility sometimes results in GTK
+ // sending multiple value-changed events. As if when Inkscape interrupts redraw for main loop
+ // iterations, GTK discovers that this callback hasn't finished yet, and for some weird reason
+ // decides to add yet another value-changed event to the queue. Totally braindead if you ask
+ // me. As a result, scrolling the spinbutton once results in runaway change until it hits 1.0
+ // or 0.0. (And no, this is not a race with ::update, I checked that.)
+ // Sigh. So we disable interruptibility while we're setting the new value.
+ _desktop->getCanvas()->forceFullRedrawAfterInterruptions(0);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", SP_VERB_DIALOG_FILL_STROKE,
+ _("Change opacity"));
+ // resume interruptibility
+ _desktop->getCanvas()->endForcedFullRedraws();
+ // spinbutton_defocus(GTK_WIDGET(_opacity_sb.gobj()));
+ _opacity_blocked = false;
+}
+
+/* ============================================= RotateableSwatch */
+
+RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode) :
+ fillstroke(mode),
+ parent(parent),
+ startcolor(0),
+ startcolor_set(false),
+ undokey("ssrot1"),
+ cr(nullptr),
+ cr_set(false)
+
+{
+}
+
+RotateableSwatch::~RotateableSwatch() = default;
+
+double
+RotateableSwatch::color_adjust(float *hsla, double by, guint32 cc, guint modifier)
+{
+ SPColor::rgb_to_hsl_floatv (hsla, SP_RGBA32_R_F(cc), SP_RGBA32_G_F(cc), SP_RGBA32_B_F(cc));
+ hsla[3] = SP_RGBA32_A_F(cc);
+ double diff = 0;
+ if (modifier == 2) { // saturation
+ double old = hsla[1];
+ if (by > 0) {
+ hsla[1] += by * (1 - hsla[1]);
+ } else {
+ hsla[1] += by * (hsla[1]);
+ }
+ diff = hsla[1] - old;
+ } else if (modifier == 1) { // lightness
+ double old = hsla[2];
+ if (by > 0) {
+ hsla[2] += by * (1 - hsla[2]);
+ } else {
+ hsla[2] += by * (hsla[2]);
+ }
+ diff = hsla[2] - old;
+ } else if (modifier == 3) { // alpha
+ double old = hsla[3];
+ hsla[3] += by/2;
+ if (hsla[3] < 0) {
+ hsla[3] = 0;
+ } else if (hsla[3] > 1) {
+ hsla[3] = 1;
+ }
+ diff = hsla[3] - old;
+ } else { // hue
+ double old = hsla[0];
+ hsla[0] += by/2;
+ while (hsla[0] < 0)
+ hsla[0] += 1;
+ while (hsla[0] > 1)
+ hsla[0] -= 1;
+ diff = hsla[0] - old;
+ }
+
+ float rgb[3];
+ SPColor::hsl_to_rgb_floatv (rgb, hsla[0], hsla[1], hsla[2]);
+
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (SP_COLOR_F_TO_U(rgb[0])),
+ (SP_COLOR_F_TO_U(rgb[1])),
+ (SP_COLOR_F_TO_U(rgb[2])),
+ 0xff
+ )
+ );
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ if (modifier == 3) { // alpha
+ Inkscape::CSSOStringStream osalpha;
+ osalpha << hsla[3];
+ sp_repr_css_set_property(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", osalpha.str().c_str());
+ } else {
+ sp_repr_css_set_property (css, (fillstroke == SS_FILL) ? "fill" : "stroke", c);
+ }
+ sp_desktop_set_style (parent->getDesktop(), css);
+ sp_repr_css_attr_unref (css);
+ return diff;
+}
+
+void
+RotateableSwatch::do_motion(double by, guint modifier) {
+ if (parent->_mode[fillstroke] != SS_COLOR)
+ return;
+
+ if (!scrolling && !cr_set) {
+ GtkWidget *w = GTK_WIDGET(gobj());
+ GdkPixbuf *pixbuf = nullptr;
+
+ if (modifier == 2) { // saturation
+ pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_s_xpm);
+ } else if (modifier == 1) { // lightness
+ pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_l_xpm);
+ } else if (modifier == 3) { // alpha
+ pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_a_xpm);
+ } else { // hue
+ pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_h_xpm);
+ }
+
+ if (pixbuf != nullptr) {
+ cr = gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, 16, 16);
+ g_object_unref(pixbuf);
+ gdk_window_set_cursor(gtk_widget_get_window(w), cr);
+ g_object_unref(cr);
+ cr = nullptr;
+ cr_set = true;
+ }
+ }
+
+ guint32 cc;
+ if (!startcolor_set) {
+ cc = startcolor = parent->_thisselected[fillstroke];
+ startcolor_set = true;
+ } else {
+ cc = startcolor;
+ }
+
+ float hsla[4];
+ double diff = 0;
+
+ diff = color_adjust(hsla, by, cc, modifier);
+
+ if (modifier == 3) { // alpha
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust alpha")));
+ double ch = hsla[3];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>alpha</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Shift</b> to adjust saturation, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else if (modifier == 2) { // saturation
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust saturation")));
+ double ch = hsla[1];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>saturation</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else if (modifier == 1) { // lightness
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust lightness")));
+ double ch = hsla[2];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>lightness</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else { // hue
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust hue")));
+ double ch = hsla[0];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>hue</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, with <b>Ctrl</b> to adjust lightness"), ch - diff, ch, diff);
+ }
+}
+
+
+void
+RotateableSwatch::do_scroll(double by, guint modifier) {
+ do_motion(by/30.0, modifier);
+ do_release(by/30.0, modifier);
+}
+
+void
+RotateableSwatch::do_release(double by, guint modifier) {
+ if (parent->_mode[fillstroke] != SS_COLOR)
+ return;
+
+ float hsla[4];
+ color_adjust(hsla, by, startcolor, modifier);
+
+ if (cr_set) {
+ GtkWidget *w = GTK_WIDGET(gobj());
+ gdk_window_set_cursor(gtk_widget_get_window(w), nullptr);
+ if (cr) {
+ g_object_unref(cr);
+ cr = nullptr;
+ }
+ cr_set = false;
+ }
+
+ if (modifier == 3) { // alpha
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, ("Adjust alpha"));
+ } else if (modifier == 2) { // saturation
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, ("Adjust saturation"));
+
+ } else if (modifier == 1) { // lightness
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, ("Adjust lightness"));
+
+ } else { // hue
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, ("Adjust hue"));
+ }
+
+ if (!strcmp(undokey, "ssrot1")) {
+ undokey = "ssrot2";
+ } else {
+ undokey = "ssrot1";
+ }
+
+ parent->getDesktop()->event_context->message_context->clear();
+ startcolor_set = false;
+}
+
+/* ============================================= RotateableStrokeWidth */
+
+RotateableStrokeWidth::RotateableStrokeWidth(SelectedStyle *parent) :
+ parent(parent),
+ startvalue(0),
+ startvalue_set(false),
+ undokey("swrot1")
+{
+}
+
+RotateableStrokeWidth::~RotateableStrokeWidth() = default;
+
+double
+RotateableStrokeWidth::value_adjust(double current, double by, guint /*modifier*/, bool final)
+{
+ double newval;
+ // by is -1..1
+ double max_f = 50; // maximum width is (current * max_f), minimum - zero
+ newval = current * (std::exp(std::log(max_f-1) * (by+1)) - 1) / (max_f-2);
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ if (final && newval < 1e-6) {
+ // if dragged into zero and this is the final adjust on mouse release, delete stroke;
+ // if it's not final, leave it a chance to increase again (which is not possible with "none")
+ sp_repr_css_set_property (css, "stroke", "none");
+ } else {
+ newval = Inkscape::Util::Quantity::convert(newval, parent->_sw_unit, "px");
+ Inkscape::CSSOStringStream os;
+ os << newval;
+ sp_repr_css_set_property (css, "stroke-width", os.str().c_str());
+ }
+
+ sp_desktop_set_style (parent->getDesktop(), css);
+ sp_repr_css_attr_unref (css);
+ return newval - current;
+}
+
+void
+RotateableStrokeWidth::do_motion(double by, guint modifier) {
+
+ // if this is the first motion after a mouse grab, remember the current width
+ if (!startvalue_set) {
+ startvalue = parent->current_stroke_width;
+ // if it's 0, adjusting (which uses multiplication) will not be able to change it, so we
+ // cheat and provide a non-zero value
+ if (startvalue == 0)
+ startvalue = 1;
+ startvalue_set = true;
+ }
+
+ if (modifier == 3) { // Alt, do nothing
+ } else {
+ double diff = value_adjust(startvalue, by, modifier, false);
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust stroke width")));
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>stroke width</b>: was %.3g, now <b>%.3g</b> (diff %.3g)"), startvalue, startvalue + diff, diff);
+ }
+}
+
+void
+RotateableStrokeWidth::do_release(double by, guint modifier) {
+
+ if (modifier == 3) { // do nothing
+
+ } else {
+ value_adjust(startvalue, by, modifier, true);
+ startvalue_set = false;
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey,
+ SP_VERB_DIALOG_FILL_STROKE, (_("Adjust stroke width")));
+ }
+
+ if (!strcmp(undokey, "swrot1")) {
+ undokey = "swrot2";
+ } else {
+ undokey = "swrot1";
+ }
+ parent->getDesktop()->event_context->message_context->clear();
+}
+
+void
+RotateableStrokeWidth::do_scroll(double by, guint modifier) {
+ do_motion(by/10.0, modifier);
+ startvalue_set = false;
+}
+
+Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop)
+{
+ if (Dialog::PanelDialogBase *panel_dialog =
+ dynamic_cast<Dialog::PanelDialogBase *>(desktop->_dlg_mgr->getDialog("FillAndStroke"))) {
+ try {
+ Dialog::FillAndStroke &fill_and_stroke =
+ dynamic_cast<Dialog::FillAndStroke &>(panel_dialog->getPanel());
+ return &fill_and_stroke;
+ } catch (std::exception &e) { }
+ }
+
+ return nullptr;
+}
+
+} // namespace Widget
+} // 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/widget/selected-style.h b/src/ui/widget/selected-style.h
new file mode 100644
index 0000000..388f802
--- /dev/null
+++ b/src/ui/widget/selected-style.h
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ * scislac@users.sf.net
+ *
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_CURRENT_STYLE_H
+#define INKSCAPE_UI_CURRENT_STYLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+
+#include <gtkmm/label.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/radiobuttongroup.h>
+#include <gtkmm/radiomenuitem.h>
+#include "ui/widget/spinbutton.h"
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+
+#include "rotateable.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+namespace Util {
+ class Unit;
+}
+
+namespace UI {
+namespace Widget {
+
+enum {
+ SS_NA,
+ SS_NONE,
+ SS_UNSET,
+ SS_PATTERN,
+ SS_LGRADIENT,
+ SS_RGRADIENT,
+#ifdef WITH_MESH
+ SS_MGRADIENT,
+#endif
+ SS_MANY,
+ SS_COLOR,
+ SS_HATCH
+};
+
+enum {
+ SS_FILL,
+ SS_STROKE
+};
+
+class SelectedStyle;
+
+class RotateableSwatch : public Rotateable {
+ public:
+ RotateableSwatch(SelectedStyle *parent, guint mode);
+ ~RotateableSwatch() override;
+
+ double color_adjust (float *hsl, double by, guint32 cc, guint state);
+
+ void do_motion (double by, guint state) override;
+ void do_release (double by, guint state) override;
+ void do_scroll (double by, guint state) override;
+
+private:
+ guint fillstroke;
+
+ SelectedStyle *parent;
+
+ guint32 startcolor;
+ bool startcolor_set;
+
+ gchar const *undokey;
+
+ GdkCursor *cr;
+ bool cr_set;
+};
+
+class RotateableStrokeWidth : public Rotateable {
+ public:
+ RotateableStrokeWidth(SelectedStyle *parent);
+ ~RotateableStrokeWidth() override;
+
+ double value_adjust(double current, double by, guint modifier, bool final);
+ void do_motion (double by, guint state) override;
+ void do_release (double by, guint state) override;
+ void do_scroll (double by, guint state) override;
+
+private:
+ SelectedStyle *parent;
+
+ double startvalue;
+ bool startvalue_set;
+
+ gchar const *undokey;
+};
+
+/**
+ * Selected style indicator (fill, stroke, opacity).
+ */
+class SelectedStyle : public Gtk::HBox
+{
+public:
+ SelectedStyle(bool layout = true);
+
+ ~SelectedStyle() override;
+
+ void setDesktop(SPDesktop *desktop);
+ SPDesktop *getDesktop() {return _desktop;}
+ void update();
+
+ guint32 _lastselected[2];
+ guint32 _thisselected[2];
+
+ guint _mode[2];
+
+ double current_stroke_width;
+ Inkscape::Util::Unit const *_sw_unit; // points to object in UnitTable, do not delete
+
+protected:
+ SPDesktop *_desktop;
+
+ Gtk::Grid _table;
+
+ Gtk::Label _fill_label;
+ Gtk::Label _stroke_label;
+ Gtk::Label _opacity_label;
+
+ RotateableSwatch _fill_place;
+ RotateableSwatch _stroke_place;
+
+ Gtk::EventBox _fill_flag_place;
+ Gtk::EventBox _stroke_flag_place;
+
+ Gtk::EventBox _opacity_place;
+ Glib::RefPtr<Gtk::Adjustment> _opacity_adjustment;
+ Inkscape::UI::Widget::SpinButton _opacity_sb;
+
+ Gtk::Label _na[2];
+ Glib::ustring __na[2];
+
+ Gtk::Label _none[2];
+ Glib::ustring __none[2];
+
+ Gtk::Label _pattern[2];
+ Glib::ustring __pattern[2];
+
+ Gtk::Label _hatch[2];
+ Glib::ustring __hatch[2];
+
+ Gtk::Label _lgradient[2];
+ Glib::ustring __lgradient[2];
+
+ GtkWidget *_gradient_preview_l[2];
+ Gtk::HBox _gradient_box_l[2];
+
+ Gtk::Label _rgradient[2];
+ Glib::ustring __rgradient[2];
+
+ GtkWidget *_gradient_preview_r[2];
+ Gtk::HBox _gradient_box_r[2];
+
+#ifdef WITH_MESH
+ Gtk::Label _mgradient[2];
+ Glib::ustring __mgradient[2];
+
+ GtkWidget *_gradient_preview_m[2];
+ Gtk::HBox _gradient_box_m[2];
+#endif
+
+ Gtk::Label _many[2];
+ Glib::ustring __many[2];
+
+ Gtk::Label _unset[2];
+ Glib::ustring __unset[2];
+
+ Gtk::Widget *_color_preview[2];
+ Glib::ustring __color[2];
+
+ Gtk::Label _averaged[2];
+ Glib::ustring __averaged[2];
+ Gtk::Label _multiple[2];
+ Glib::ustring __multiple[2];
+
+ Gtk::HBox _fill;
+ Gtk::HBox _stroke;
+ RotateableStrokeWidth _stroke_width_place;
+ Gtk::Label _stroke_width;
+ Gtk::Label _fill_empty_space;
+
+ Glib::ustring _paintserver_id[2];
+
+ sigc::connection *selection_changed_connection;
+ sigc::connection *selection_modified_connection;
+ sigc::connection *subselection_changed_connection;
+
+ static void dragDataReceived( GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint event_time,
+ gpointer user_data );
+
+ bool on_fill_click(GdkEventButton *event);
+ bool on_stroke_click(GdkEventButton *event);
+ bool on_opacity_click(GdkEventButton *event);
+ bool on_sw_click(GdkEventButton *event);
+
+ bool _opacity_blocked;
+ void on_opacity_changed();
+ void on_opacity_menu(Gtk::Menu *menu);
+ void opacity_0();
+ void opacity_025();
+ void opacity_05();
+ void opacity_075();
+ void opacity_1();
+
+ void on_fill_remove();
+ void on_stroke_remove();
+ void on_fill_lastused();
+ void on_stroke_lastused();
+ void on_fill_lastselected();
+ void on_stroke_lastselected();
+ void on_fill_unset();
+ void on_stroke_unset();
+ void on_fill_edit();
+ void on_stroke_edit();
+ void on_fillstroke_swap();
+ void on_fill_invert();
+ void on_stroke_invert();
+ void on_fill_white();
+ void on_stroke_white();
+ void on_fill_black();
+ void on_stroke_black();
+ void on_fill_copy();
+ void on_stroke_copy();
+ void on_fill_paste();
+ void on_stroke_paste();
+ void on_fill_opaque();
+ void on_stroke_opaque();
+
+ Gtk::Menu _popup[2];
+ Gtk::MenuItem _popup_edit[2];
+ Gtk::MenuItem _popup_lastused[2];
+ Gtk::MenuItem _popup_lastselected[2];
+ Gtk::MenuItem _popup_invert[2];
+ Gtk::MenuItem _popup_white[2];
+ Gtk::MenuItem _popup_black[2];
+ Gtk::MenuItem _popup_copy[2];
+ Gtk::MenuItem _popup_paste[2];
+ Gtk::MenuItem _popup_swap[2];
+ Gtk::MenuItem _popup_opaque[2];
+ Gtk::MenuItem _popup_unset[2];
+ Gtk::MenuItem _popup_remove[2];
+
+ Gtk::Menu _popup_sw;
+ Gtk::RadioButtonGroup _sw_group;
+ std::vector<Gtk::RadioMenuItem*> _unit_mis;
+ void on_popup_units(Inkscape::Util::Unit const *u);
+ void on_popup_preset(int i);
+ Gtk::MenuItem _popup_sw_remove;
+
+ void *_drop[2];
+ bool _dropEnabled[2];
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_BUTTON_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/widget/spin-button-tool-item.cpp b/src/ui/widget/spin-button-tool-item.cpp
new file mode 100644
index 0000000..b283939
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.cpp
@@ -0,0 +1,532 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "spin-button-tool-item.h"
+
+#include <gtkmm/box.h>
+#include <gtkmm/image.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/toolbar.h>
+
+#include <utility>
+
+#include "spinbutton.h"
+#include "ui/icon-loader.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief Handler for the button's "focus-in-event" signal
+ *
+ * \param focus_event The event that triggered the signal
+ *
+ * \detail This just logs the current value of the spin-button
+ * and sets the _transfer_focus flag
+ */
+bool
+SpinButtonToolItem::on_btn_focus_in_event(GdkEventFocus * /* focus_event */)
+{
+ _last_val = _btn->get_value();
+ _transfer_focus = true;
+
+ return false; // Event not consumed
+}
+
+/**
+ * \brief Handler for the button's "focus-out-event" signal
+ *
+ * \param focus_event The event that triggered the signal
+ *
+ * \detail This just unsets the _transfer_focus flag
+ */
+bool
+SpinButtonToolItem::on_btn_focus_out_event(GdkEventFocus * /* focus_event */)
+{
+ _transfer_focus = false;
+
+ return false; // Event not consumed
+}
+
+/**
+ * \brief Handler for the button's "key-press-event" signal
+ *
+ * \param key_event The event that triggered the signal
+ *
+ * \detail If the ESC key was pressed, restore the last value and defocus.
+ * If the Enter key was pressed, just defocus.
+ */
+bool
+SpinButtonToolItem::on_btn_key_press_event(GdkEventKey *key_event)
+{
+ bool was_consumed = false; // Whether event has been consumed or not
+ auto display = Gdk::Display::get_default();
+ auto keymap = display->get_keymap();
+ guint key = 0;
+ gdk_keymap_translate_keyboard_state(keymap, key_event->hardware_keycode,
+ static_cast<GdkModifierType>(key_event->state),
+ 0, &key, 0, 0, 0);
+
+ auto val = _btn->get_value();
+
+ switch(key) {
+ case GDK_KEY_Escape:
+ {
+ _transfer_focus = true;
+ _btn->set_value(_last_val);
+ defocus();
+ was_consumed = true;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ _transfer_focus = true;
+ defocus();
+ was_consumed = true;
+ }
+ break;
+
+ case GDK_KEY_Tab:
+ {
+ _transfer_focus = false;
+ was_consumed = process_tab(1);
+ }
+ break;
+
+ case GDK_KEY_ISO_Left_Tab:
+ {
+ _transfer_focus = false;
+ was_consumed = process_tab(-1);
+ }
+ break;
+
+ // TODO: Enable variable step-size if this is ever used
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val+1);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val-1);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val+10);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val-10);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ {
+ _transfer_focus = false;
+ _btn->set_value(_last_val);
+ was_consumed = true;
+ }
+ break;
+ }
+
+ return was_consumed;
+}
+
+/**
+ * \brief Shift focus to a different widget
+ *
+ * \details This only has an effect if the _transfer_focus flag and the _focus_widget are set
+ */
+void
+SpinButtonToolItem::defocus()
+{
+ if(_transfer_focus && _focus_widget) {
+ _focus_widget->grab_focus();
+ }
+}
+
+/**
+ * \brief Move focus to another spinbutton in the toolbar
+ *
+ * \param increment[in] The number of places to shift within the toolbar
+ */
+bool
+SpinButtonToolItem::process_tab(int increment)
+{
+ // If the increment is zero, do nothing
+ if(increment == 0) return true;
+
+ // Here, we're working through the widget hierarchy:
+ // Toolbar
+ // |- ToolItem (*this)
+ // |-> Box
+ // |-> SpinButton (*_btn)
+ //
+ // Our aim is to find the next/previous spin-button within a toolitem in our toolbar
+
+ bool handled = false;
+
+ // We only bother doing this if the current item is actually in a toolbar!
+ auto toolbar = dynamic_cast<Gtk::Toolbar *>(get_parent());
+
+ if (toolbar) {
+ // Get the index of the current item within the toolbar and the total number of items
+ auto my_index = toolbar->get_item_index(*this);
+ auto n_items = toolbar->get_n_items();
+
+ auto test_index = my_index + increment; // The index of the item we want to check
+
+ // Loop through tool items as long as we're within the bounds of the toolbar and
+ // we haven't yet found our new item to focus on
+ while(test_index > 0 && test_index <= n_items && !handled) {
+
+ auto tool_item = toolbar->get_nth_item(test_index);
+
+ if(tool_item) {
+ // There are now two options that we support:
+ if(dynamic_cast<SpinButtonToolItem *>(tool_item)) {
+ // (1) The tool item is a SpinButtonToolItem, in which case, we just pass
+ // focus to its spin-button
+ dynamic_cast<SpinButtonToolItem *>(tool_item)->grab_button_focus();
+ handled = true;
+ }
+ else if(dynamic_cast<Gtk::SpinButton *>(tool_item->get_child())) {
+ // (2) The tool item contains a plain Gtk::SpinButton, in which case we
+ // pass focus directly to it
+ tool_item->get_child()->grab_focus();
+ }
+ }
+
+ test_index += increment;
+ }
+ }
+
+ return handled;
+}
+
+/**
+ * \brief Handler for toggle events on numeric menu items
+ *
+ * \details Sets the adjustment to the desired value
+ */
+void
+SpinButtonToolItem::on_numeric_menu_item_toggled(double value)
+{
+ auto adj = _btn->get_adjustment();
+ adj->set_value(value);
+}
+
+Gtk::RadioMenuItem *
+SpinButtonToolItem::create_numeric_menu_item(Gtk::RadioButtonGroup *group,
+ double value,
+ const Glib::ustring& label)
+{
+ // Represent the value as a string
+ std::ostringstream ss;
+ ss << value;
+
+ // Append the label if specified
+ if (!label.empty()) {
+ ss << ": " << label;
+ }
+
+ auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, ss.str()));
+
+ // Set the adjustment value in response to changes in the selected item
+ auto toggled_handler = sigc::bind(sigc::mem_fun(*this, &SpinButtonToolItem::on_numeric_menu_item_toggled), value);
+ numeric_option->signal_toggled().connect(toggled_handler);
+
+ return numeric_option;
+}
+
+/**
+ * \brief Create a menu containing fixed numeric options for the adjustment
+ *
+ * \details Each of these values represents a snap-point for the adjustment's value
+ */
+Gtk::Menu *
+SpinButtonToolItem::create_numeric_menu()
+{
+ auto numeric_menu = Gtk::manage(new Gtk::Menu());
+
+ Gtk::RadioMenuItem::Group group;
+
+ // Get values for the adjustment
+ auto adj = _btn->get_adjustment();
+ auto adj_value = adj->get_value();
+ auto lower = adj->get_lower();
+ auto upper = adj->get_upper();
+ auto step = adj->get_step_increment();
+ auto page = adj->get_page_increment();
+
+ auto digits = _btn->get_digits();
+
+ // A number a little smaller than the smallest increment that can be
+ // displayed in the spinbutton entry.
+ //
+ // For example, if digits = 0, we are displaying integers only and
+ // epsilon = 0.9 * 10^-0 = 0.9
+ //
+ // For digits = 1, we get epsilon = 0.9 * 10^-1 = 0.09
+ // For digits = 2, we get epsilon = 0.9 * 10^-2 = 0.009 etc...
+ auto epsilon = 0.9 * pow(10.0, -float(digits));
+
+ // Start by setting some fixed values based on the adjustment's
+ // parameters.
+ NumericMenuData values;
+ values.push_back(std::make_pair(upper, ""));
+ values.push_back(std::make_pair(adj_value + page, ""));
+ values.push_back(std::make_pair(adj_value + step, ""));
+ values.push_back(std::make_pair(adj_value, ""));
+ values.push_back(std::make_pair(adj_value - step, ""));
+ values.push_back(std::make_pair(adj_value - page, ""));
+ values.push_back(std::make_pair(lower, ""));
+
+ // Now add any custom items
+ for (auto custom_data : _custom_menu_data) {
+ values.push_back(custom_data);
+ }
+
+ // Sort the numeric menu items into reverse numerical order (largest at top of menu)
+ std::sort (begin(values), end(values));
+ std::reverse(begin(values), end(values));
+
+ for (auto value : values)
+ {
+ auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second);
+ numeric_menu->append(*numeric_menu_item);
+
+ if (fabs(adj_value - value.first) < epsilon) {
+ // If the adjustment value is very close to the value of this menu item,
+ // make this menu item active
+ numeric_menu_item->set_active();
+ }
+ }
+
+ return numeric_menu;
+}
+
+/**
+ * \brief Create a menu-item in response to the "create-menu-proxy" signal
+ *
+ * \detail This is an override for the default Gtk::ToolItem handler so
+ * we don't need to explicitly connect this to the signal. It
+ * runs if the toolitem is unable to fit on the toolbar, and
+ * must be represented by a menu item instead.
+ */
+bool
+SpinButtonToolItem::on_create_menu_proxy()
+{
+ // The main menu-item. It just contains the label that normally appears
+ // next to the spin-button, and an indicator for a sub-menu.
+ auto menu_item = Gtk::manage(new Gtk::MenuItem(_label_text));
+ auto numeric_menu = create_numeric_menu();
+ menu_item->set_submenu(*numeric_menu);
+
+ set_proxy_menu_item(_name, *menu_item);
+
+ return true; // Finished handling the event
+}
+
+/**
+ * \brief Create a new SpinButtonToolItem
+ *
+ * \param[in] name A unique ID for this tool-item (not translatable)
+ * \param[in] label_text The text to display in the toolbar
+ * \param[in] adjustment The Gtk::Adjustment to attach to the spinbutton
+ * \param[in] climb_rate The climb rate for the spin button (default = 0)
+ * \param[in] digits Number of decimal places to display
+ */
+SpinButtonToolItem::SpinButtonToolItem(const Glib::ustring name,
+ const Glib::ustring& label_text,
+ Glib::RefPtr<Gtk::Adjustment>& adjustment,
+ double climb_rate,
+ int digits)
+ : _btn(Gtk::manage(new SpinButton(adjustment, climb_rate, digits))),
+ _name(std::move(name)),
+ _label_text(label_text),
+ _last_val(0.0),
+ _transfer_focus(false),
+ _focus_widget(nullptr)
+{
+ set_margin_start(3);
+ set_margin_end(3);
+ set_name(_name);
+
+ // Handle popup menu
+ _btn->signal_popup_menu().connect(sigc::mem_fun(*this, &SpinButtonToolItem::on_popup_menu), false);
+
+ // Handle button events
+ auto btn_focus_in_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_in_event);
+ _btn->signal_focus_in_event().connect(btn_focus_in_event_cb, false);
+
+ auto btn_focus_out_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_out_event);
+ _btn->signal_focus_out_event().connect(btn_focus_out_event_cb, false);
+
+ auto btn_key_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_key_press_event);
+ _btn->signal_key_press_event().connect(btn_key_press_event_cb, false);
+
+ auto btn_button_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_button_press_event);
+ _btn->signal_button_press_event().connect(btn_button_press_event_cb, false);
+
+ _btn->add_events(Gdk::KEY_PRESS_MASK);
+
+ // Create a label
+ _label = Gtk::manage(new Gtk::Label(label_text));
+
+ // Arrange the widgets in a horizontal box
+ _hbox = Gtk::manage(new Gtk::Box());
+ _hbox->set_spacing(3);
+ _hbox->pack_start(*_label);
+ _hbox->pack_start(*_btn);
+ add(*_hbox);
+ show_all();
+}
+
+void
+SpinButtonToolItem::set_icon(const Glib::ustring& icon_name)
+{
+ _hbox->remove(*_label);
+ _icon = Gtk::manage(sp_get_icon_image(icon_name, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ if(_icon) {
+ _hbox->pack_start(*_icon);
+ _hbox->reorder_child(*_icon, 0);
+ }
+
+ show_all();
+}
+
+bool
+SpinButtonToolItem::on_btn_button_press_event(const GdkEventButton *button_event)
+{
+ if (gdk_event_triggers_context_menu(reinterpret_cast<const GdkEvent *>(button_event)) &&
+ button_event->type == GDK_BUTTON_PRESS) {
+ do_popup_menu(button_event);
+ return true;
+ }
+
+ return false;
+}
+
+void
+SpinButtonToolItem::do_popup_menu(const GdkEventButton *button_event)
+{
+ auto menu = create_numeric_menu();
+ menu->attach_to_widget(*_btn);
+ menu->show_all();
+ menu->popup_at_pointer(reinterpret_cast<const GdkEvent *>(button_event));
+}
+
+/**
+ * \brief Create a popup menu
+ */
+bool
+SpinButtonToolItem::on_popup_menu()
+{
+ do_popup_menu(nullptr);
+ return true;
+}
+
+/**
+ * \brief Transfers focus to the child spinbutton by default
+ */
+void
+SpinButtonToolItem::on_grab_focus()
+{
+ grab_button_focus();
+}
+
+/**
+ * \brief Set the tooltip to display on this (and all child widgets)
+ *
+ * \param[in] text The tooltip to display
+ */
+void
+SpinButtonToolItem::set_all_tooltip_text(const Glib::ustring& text)
+{
+ set_tooltip_text(text);
+ _btn->set_tooltip_text(text);
+}
+
+/**
+ * \brief Set the widget that focus moves to when this one loses focus
+ *
+ * \param widget The widget that will gain focus
+ */
+void
+SpinButtonToolItem::set_focus_widget(Gtk::Widget *widget)
+{
+ _focus_widget = widget;
+}
+
+/**
+ * \brief Grab focus on the spin-button widget
+ */
+void
+SpinButtonToolItem::grab_button_focus()
+{
+ _btn->grab_focus();
+}
+
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(std::vector<double>& values,
+ const std::vector<Glib::ustring>& labels)
+{
+ if(values.size() != labels.size() && !labels.empty()) {
+ g_warning("Cannot add custom menu items. Value and label arrays are different sizes");
+ return;
+ }
+
+ _custom_menu_data.clear();
+
+ int i = 0;
+
+ for (auto value : values) {
+ if(labels.empty()) {
+ _custom_menu_data.push_back(std::make_pair(value, ""));
+ }
+ else {
+ _custom_menu_data.push_back(std::make_pair(value, labels[i++]));
+ }
+ }
+}
+
+Glib::RefPtr<Gtk::Adjustment>
+SpinButtonToolItem::get_adjustment()
+{
+ return _btn->get_adjustment();
+}
+} // namespace Widget
+} // 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/widget/spin-button-tool-item.h b/src/ui/widget/spin-button-tool-item.h
new file mode 100644
index 0000000..c073f56
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.h
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SPIN_BUTTON_TOOL_ITEM_H
+#define SEEN_SPIN_BUTTON_TOOL_ITEM_H
+
+#include <gtkmm/toolitem.h>
+
+namespace Gtk {
+class Box;
+class RadioButtonGroup;
+class RadioMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class SpinButton;
+
+/**
+ * \brief A spin-button with a label that can be added to a toolbar
+ */
+class SpinButtonToolItem : public Gtk::ToolItem
+{
+private:
+ typedef std::vector< std::pair<double, Glib::ustring> > NumericMenuData;
+
+ Glib::ustring _name; ///< A unique ID for the widget (NOT translatable)
+ SpinButton *_btn; ///< The spin-button within the widget
+ Glib::ustring _label_text; ///< A string to use in labels for the widget (translatable)
+ double _last_val; ///< The last value of the adjustment
+ bool _transfer_focus; ///< Whether or not to transfer focus
+
+ Gtk::Box *_hbox; ///< Horizontal box, to store widgets
+ Gtk::Widget *_label; ///< A text label to describe the setting
+ Gtk::Widget *_icon; ///< An icon to describe the setting
+
+ /** A widget that grabs focus when this one loses it */
+ Gtk::Widget * _focus_widget;
+
+ // Custom values and labels to add to the numeric popup-menu
+ NumericMenuData _custom_menu_data;
+
+ // Event handlers
+ bool on_btn_focus_in_event(GdkEventFocus *focus_event);
+ bool on_btn_focus_out_event(GdkEventFocus *focus_event);
+ bool on_btn_key_press_event(GdkEventKey *key_event);
+ bool on_btn_button_press_event(const GdkEventButton *button_event);
+ bool on_popup_menu();
+ void do_popup_menu(const GdkEventButton *button_event);
+
+ void defocus();
+ bool process_tab(int direction);
+
+ void on_numeric_menu_item_toggled(double value);
+
+ Gtk::Menu * create_numeric_menu();
+
+ Gtk::RadioMenuItem * create_numeric_menu_item(Gtk::RadioButtonGroup *group,
+ double value,
+ const Glib::ustring& label = "");
+
+protected:
+ bool on_create_menu_proxy() override;
+ void on_grab_focus() override;
+
+public:
+ SpinButtonToolItem(const Glib::ustring name,
+ const Glib::ustring& label_text,
+ Glib::RefPtr<Gtk::Adjustment>& adjustment,
+ double climb_rate = 0.1,
+ int digits = 3);
+
+ void set_all_tooltip_text(const Glib::ustring& text);
+ void set_focus_widget(Gtk::Widget *widget);
+ void grab_button_focus();
+
+ void set_custom_numeric_menu_data(std::vector<double>& values,
+ const std::vector<Glib::ustring>& labels = std::vector<Glib::ustring>());
+ Glib::RefPtr<Gtk::Adjustment> get_adjustment();
+ void set_icon(const Glib::ustring& icon_name);
+};
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#endif // SEEN_SPIN_BUTTON_TOOL_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/widget/spin-scale.cpp b/src/ui/widget/spin-scale.cpp
new file mode 100644
index 0000000..5e3a2a2
--- /dev/null
+++ b/src/ui/widget/spin-scale.cpp
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2012 Author
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spin-scale.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SpinScale::SpinScale(const Glib::ustring label, double value,
+ double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttributeEnum a, const Glib::ustring tip_text)
+ : AttrWidget(a, value)
+ , _inkspinscale(value, lower, upper, step_increment, page_increment, 0)
+{
+ set_name("SpinScale");
+
+ _inkspinscale.set_label (label);
+ _inkspinscale.set_digits (digits);
+ _inkspinscale.set_tooltip_text (tip_text);
+
+ _adjustment = _inkspinscale.get_adjustment();
+
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ pack_start(_inkspinscale);
+
+ show_all_children();
+}
+
+SpinScale::SpinScale(const Glib::ustring label,
+ Glib::RefPtr<Gtk::Adjustment> adjustment, int digits,
+ const SPAttributeEnum a, const Glib::ustring tip_text)
+ : AttrWidget(a, 0.0)
+ , _inkspinscale(adjustment)
+{
+ set_name("SpinScale");
+
+ _inkspinscale.set_label (label);
+ _inkspinscale.set_digits (digits);
+ _inkspinscale.set_tooltip_text (tip_text);
+
+ _adjustment = _inkspinscale.get_adjustment();
+
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ pack_start(_inkspinscale);
+
+ show_all_children();
+}
+
+Glib::ustring SpinScale::get_as_attribute() const
+{
+ const double val = _adjustment->get_value();
+
+ if( _inkspinscale.get_digits() == 0)
+ return Glib::Ascii::dtostr((int)val);
+ else
+ return Glib::Ascii::dtostr(val);
+}
+
+void SpinScale::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if (val)
+ _adjustment->set_value(Glib::Ascii::strtod(val));
+ else
+ _adjustment->set_value(get_default()->as_double());
+}
+
+Glib::SignalProxy0<void> SpinScale::signal_value_changed()
+{
+ return _adjustment->signal_value_changed();
+}
+
+double SpinScale::get_value() const
+{
+ return _adjustment->get_value();
+}
+
+void SpinScale::set_value(const double val)
+{
+ _adjustment->set_value(val);
+}
+
+void SpinScale::set_focuswidget(GtkWidget *widget)
+{
+ _inkspinscale.set_focus_widget(widget);
+}
+
+const decltype(SpinScale::_adjustment) SpinScale::get_adjustment() const
+{
+ return _adjustment;
+}
+
+decltype(SpinScale::_adjustment) SpinScale::get_adjustment()
+{
+ return _adjustment;
+}
+
+
+DualSpinScale::DualSpinScale(const Glib::ustring label1, const Glib::ustring label2,
+ double value, double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttributeEnum a,
+ const Glib::ustring tip_text1, const Glib::ustring tip_text2)
+ : AttrWidget(a),
+ _s1(label1, value, lower, upper, step_increment, page_increment, digits, SP_ATTR_INVALID, tip_text1),
+ _s2(label2, value, lower, upper, step_increment, page_increment, digits, SP_ATTR_INVALID, tip_text2),
+ //TRANSLATORS: "Link" means to _link_ two sliders together
+ _link(C_("Sliders", "Link"))
+{
+ set_name("DualSpinScale");
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinScale::update_linked));
+
+ _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled));
+
+ Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ vb->add(_s1);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ _link.set_active(true);
+
+ show_all();
+}
+
+Glib::ustring DualSpinScale::get_as_attribute() const
+{
+ if(_link.get_active())
+ return _s1.get_as_attribute();
+ else
+ return _s1.get_as_attribute() + " " + _s2.get_as_attribute();
+}
+
+void DualSpinScale::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if(val) {
+ // Split val into parts
+ gchar** toks = g_strsplit(val, " ", 2);
+
+ if(toks) {
+ double v1 = 0.0, v2 = 0.0;
+ if(toks[0])
+ v1 = v2 = Glib::Ascii::strtod(toks[0]);
+ if(toks[1])
+ v2 = Glib::Ascii::strtod(toks[1]);
+
+ _link.set_active(toks[1] == nullptr);
+
+ _s1.get_adjustment()->set_value(v1);
+ _s2.get_adjustment()->set_value(v2);
+
+ g_strfreev(toks);
+ }
+ }
+}
+
+sigc::signal<void>& DualSpinScale::signal_value_changed()
+{
+ return _signal_value_changed;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale1() const
+{
+ return _s1;
+}
+
+SpinScale& DualSpinScale::get_SpinScale1()
+{
+ return _s1;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale2() const
+{
+ return _s2;
+}
+
+SpinScale& DualSpinScale::get_SpinScale2()
+{
+ return _s2;
+}
+
+void DualSpinScale::link_toggled()
+{
+ _s2.set_sensitive(!_link.get_active());
+ update_linked();
+}
+
+void DualSpinScale::update_linked()
+{
+ if(_link.get_active())
+ _s2.set_value(_s1.get_value());
+}
+
+
+} // namespace Widget
+} // 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/widget/spin-scale.h b/src/ui/widget/spin-scale.h
new file mode 100644
index 0000000..b154cb3
--- /dev/null
+++ b/src/ui/widget/spin-scale.h
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2012 Author
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SPIN_SCALE_H
+#define INKSCAPE_UI_WIDGET_SPIN_SCALE_H
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/togglebutton.h>
+#include "attr-widget.h"
+#include "ink-spinscale.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Wrap the InkSpinScale class and attach an attribute.
+ * A combo widget with label, scale slider, spinbutton, and adjustment;
+ */
+class SpinScale : public Gtk::Box, public AttrWidget
+{
+
+public:
+ SpinScale(const Glib::ustring label, double value,
+ double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttributeEnum a = SP_ATTR_INVALID, const Glib::ustring tip_text = "");
+
+ // Used by extensions
+ SpinScale(const Glib::ustring label,
+ Glib::RefPtr<Gtk::Adjustment> adjustment, int digits,
+ const SPAttributeEnum a = SP_ATTR_INVALID, const Glib::ustring tip_text = "");
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ // Shortcuts to _adjustment
+ Glib::SignalProxy0<void> signal_value_changed();
+ double get_value() const;
+ void set_value(const double);
+ void set_focuswidget(GtkWidget *widget);
+
+private:
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ InkSpinScale _inkspinscale;
+
+public:
+ const decltype(_adjustment) get_adjustment() const;
+ decltype(_adjustment) get_adjustment();
+};
+
+
+/**
+ * Contains two SpinScales for controlling number-opt-number attributes.
+ *
+ * @see SpinScale
+ */
+class DualSpinScale : public Gtk::Box, public AttrWidget
+{
+public:
+ DualSpinScale(const Glib::ustring label1, const Glib::ustring label2,
+ double value, double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttributeEnum a,
+ const Glib::ustring tip_text1, const Glib::ustring tip_text2);
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ sigc::signal<void>& signal_value_changed();
+
+ const SpinScale& get_SpinScale1() const;
+ SpinScale& get_SpinScale1();
+
+ const SpinScale& get_SpinScale2() const;
+ SpinScale& get_SpinScale2();
+
+ //void remove_scale();
+private:
+ void link_toggled();
+ void update_linked();
+ sigc::signal<void> _signal_value_changed;
+ SpinScale _s1, _s2;
+ Gtk::ToggleButton _link;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPIN_SCALE_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/widget/spin-slider.cpp b/src/ui/widget/spin-slider.cpp
new file mode 100644
index 0000000..e4cd0c6
--- /dev/null
+++ b/src/ui/widget/spin-slider.cpp
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spin-slider.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SpinSlider::SpinSlider(double value, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttributeEnum a, const char* tip_text)
+ : AttrWidget(a, value),
+ _adjustment(Gtk::Adjustment::create(value, lower, upper, step_inc)),
+ _scale(_adjustment), _spin(_adjustment, climb_rate, digits)
+{
+ set_name("SpinSlider");
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ pack_start(_scale);
+ pack_start(_spin, false, false);
+ if (tip_text){
+ _scale.set_tooltip_text(tip_text);
+ _spin.set_tooltip_text(tip_text);
+ }
+
+ _scale.set_draw_value(false);
+
+ show_all_children();
+}
+
+Glib::ustring SpinSlider::get_as_attribute() const
+{
+ const auto val = _adjustment->get_value();
+
+ if(_spin.get_digits() == 0)
+ return Glib::Ascii::dtostr((int)val);
+ else
+ return Glib::Ascii::dtostr(val);
+}
+
+void SpinSlider::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if(val)
+ _adjustment->set_value(Glib::Ascii::strtod(val));
+ else
+ _adjustment->set_value(get_default()->as_double());
+}
+
+Glib::SignalProxy0<void> SpinSlider::signal_value_changed()
+{
+ return _adjustment->signal_value_changed();
+}
+
+double SpinSlider::get_value() const
+{
+ return _adjustment->get_value();
+}
+
+void SpinSlider::set_value(const double val)
+{
+ _adjustment->set_value(val);
+}
+
+const decltype(SpinSlider::_adjustment) SpinSlider::get_adjustment() const
+{
+ return _adjustment;
+}
+
+decltype(SpinSlider::_adjustment) SpinSlider::get_adjustment()
+{
+ return _adjustment;
+}
+
+const Gtk::Scale& SpinSlider::get_scale() const
+{
+ return _scale;
+}
+
+Gtk::Scale& SpinSlider::get_scale()
+{
+ return _scale;
+}
+
+const Inkscape::UI::Widget::SpinButton& SpinSlider::get_spin_button() const
+{
+ return _spin;
+}
+Inkscape::UI::Widget::SpinButton& SpinSlider::get_spin_button()
+{
+ return _spin;
+}
+
+void SpinSlider::remove_scale()
+{
+ remove(_scale);
+}
+
+DualSpinSlider::DualSpinSlider(double value, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttributeEnum a, char* tip_text1, char* tip_text2)
+ : AttrWidget(a),
+ _s1(value, lower, upper, step_inc, climb_rate, digits, SP_ATTR_INVALID, tip_text1),
+ _s2(value, lower, upper, step_inc, climb_rate, digits, SP_ATTR_INVALID, tip_text2),
+ //TRANSLATORS: "Link" means to _link_ two sliders together
+ _link(C_("Sliders", "Link"))
+{
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinSlider::update_linked));
+ _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinSlider::link_toggled));
+
+ Gtk::VBox* vb = Gtk::manage(new Gtk::VBox);
+ vb->add(_s1);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ _link.set_active(true);
+
+ show_all();
+}
+
+Glib::ustring DualSpinSlider::get_as_attribute() const
+{
+ if(_link.get_active())
+ return _s1.get_as_attribute();
+ else
+ return _s1.get_as_attribute() + " " + _s2.get_as_attribute();
+}
+
+void DualSpinSlider::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if(val) {
+ // Split val into parts
+ gchar** toks = g_strsplit(val, " ", 2);
+
+ if(toks) {
+ double v1 = 0.0, v2 = 0.0;
+ if(toks[0])
+ v1 = v2 = Glib::Ascii::strtod(toks[0]);
+ if(toks[1])
+ v2 = Glib::Ascii::strtod(toks[1]);
+
+ _link.set_active(toks[1] == nullptr);
+
+ _s1.get_adjustment()->set_value(v1);
+ _s2.get_adjustment()->set_value(v2);
+
+ g_strfreev(toks);
+ }
+ }
+}
+
+sigc::signal<void>& DualSpinSlider::signal_value_changed()
+{
+ return _signal_value_changed;
+}
+
+const SpinSlider& DualSpinSlider::get_spinslider1() const
+{
+ return _s1;
+}
+
+SpinSlider& DualSpinSlider::get_spinslider1()
+{
+ return _s1;
+}
+
+const SpinSlider& DualSpinSlider::get_spinslider2() const
+{
+ return _s2;
+}
+
+SpinSlider& DualSpinSlider::get_spinslider2()
+{
+ return _s2;
+}
+
+void DualSpinSlider::remove_scale()
+{
+ _s1.remove_scale();
+ _s2.remove_scale();
+}
+
+void DualSpinSlider::link_toggled()
+{
+ _s2.set_sensitive(!_link.get_active());
+ update_linked();
+}
+
+void DualSpinSlider::update_linked()
+{
+ if(_link.get_active())
+ _s2.set_value(_s1.get_value());
+}
+
+} // namespace Widget
+} // 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/widget/spin-slider.h b/src/ui/widget/spin-slider.h
new file mode 100644
index 0000000..24a18a0
--- /dev/null
+++ b/src/ui/widget/spin-slider.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SPIN_SLIDER_H
+#define INKSCAPE_UI_WIDGET_SPIN_SLIDER_H
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/togglebutton.h>
+#include "spinbutton.h"
+#include "attr-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Groups an HScale and a SpinButton together using the same Adjustment.
+ */
+class SpinSlider : public Gtk::HBox, public AttrWidget
+{
+public:
+ SpinSlider(double value, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttributeEnum a = SP_ATTR_INVALID, const char* tip_text = nullptr);
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ // Shortcuts to _adjustment
+ Glib::SignalProxy0<void> signal_value_changed();
+ double get_value() const;
+ void set_value(const double);
+
+ const Gtk::Scale& get_scale() const;
+ Gtk::Scale& get_scale();
+
+ const Inkscape::UI::Widget::SpinButton& get_spin_button() const;
+ Inkscape::UI::Widget::SpinButton& get_spin_button();
+
+ // Change the SpinSlider into a SpinButton with AttrWidget support)
+ void remove_scale();
+private:
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ Gtk::Scale _scale;
+ Inkscape::UI::Widget::SpinButton _spin;
+
+public:
+ const decltype(_adjustment) get_adjustment() const;
+ decltype(_adjustment) get_adjustment();
+};
+
+/**
+ * Contains two SpinSliders for controlling number-opt-number attributes.
+ *
+ * @see SpinSlider
+ */
+class DualSpinSlider : public Gtk::HBox, public AttrWidget
+{
+public:
+ DualSpinSlider(double value, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttributeEnum, char* tip_text1, char* tip_text2);
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ sigc::signal<void>& signal_value_changed();
+
+ const SpinSlider& get_spinslider1() const;
+ SpinSlider& get_spinslider1();
+
+ const SpinSlider& get_spinslider2() const;
+ SpinSlider& get_spinslider2();
+
+ void remove_scale();
+private:
+ void link_toggled();
+ void update_linked();
+ sigc::signal<void> _signal_value_changed;
+ SpinSlider _s1, _s2;
+ Gtk::ToggleButton _link;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPIN_SLIDER_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/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp
new file mode 100644
index 0000000..c633035
--- /dev/null
+++ b/src/ui/widget/spinbutton.cpp
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2011 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spinbutton.h"
+#include "unit-menu.h"
+#include "unit-tracker.h"
+#include "util/expression-evaluator.h"
+#include "ui/tools/tool-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+void
+SpinButton::connect_signals() {
+ signal_input().connect(sigc::mem_fun(*this, &SpinButton::on_input));
+ signal_focus_in_event().connect(sigc::mem_fun(*this, &SpinButton::on_my_focus_in_event));
+ signal_key_press_event().connect(sigc::mem_fun(*this, &SpinButton::on_my_key_press_event));
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &SpinButton::on_scroll_event));
+ set_focus_on_click(true);
+};
+
+int SpinButton::on_input(double* newvalue)
+{
+ try {
+ Inkscape::Util::EvaluatorQuantity result;
+ if (_unit_menu || _unit_tracker) {
+ Unit const *unit = nullptr;
+ if (_unit_menu) {
+ unit = _unit_menu->getUnit();
+ } else {
+ unit = _unit_tracker->getActiveUnit();
+ }
+ Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), unit);
+ result = eval.evaluate();
+ // check if output dimension corresponds to input unit
+ if (result.dimension != (unit->isAbsolute() ? 1 : 0) ) {
+ throw Inkscape::Util::EvaluatorException("Input dimensions do not match with parameter dimensions.","");
+ }
+ } else {
+ Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr);
+ result = eval.evaluate();
+ }
+ *newvalue = result.value;
+ }
+ catch(Inkscape::Util::EvaluatorException &e) {
+ g_message ("%s", e.what());
+
+ return false;
+ }
+
+ return true;
+}
+
+bool SpinButton::on_my_focus_in_event(GdkEventFocus* /*event*/)
+{
+ _on_focus_in_value = get_value();
+ return false; // do not consume the event
+}
+
+
+
+bool SpinButton::on_scroll_event(GdkEventScroll *event)
+{
+ if (!is_focus()) {
+ return false;
+ }
+ double step, page;
+ get_increments(step, page);
+ if (event->state & GDK_CONTROL_MASK) {
+ step = page;
+ }
+ double change = 0.0;
+ if (event->direction == GDK_SCROLL_UP) {
+ change = step;
+ } else if (event->direction == GDK_SCROLL_DOWN) {
+ change = -step;
+ } else if (event->direction == GDK_SCROLL_SMOOTH) {
+ double delta_y_clamped = CLAMP(event->delta_y, -1, 1); // values > 1 result in excessive changes
+ change = step * -delta_y_clamped;
+ } else {
+ return false;
+ }
+ set_value(get_value() + change);
+ return true;
+}
+
+bool SpinButton::on_my_key_press_event(GdkEventKey* event)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Escape:
+ undo();
+ return true; // I consumed the event
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (event->state & GDK_CONTROL_MASK) {
+ undo();
+ return true; // I consumed the event
+ }
+ break;
+ default:
+ break;
+ }
+
+ return false; // do not consume the event
+}
+
+void SpinButton::undo()
+{
+ set_value(_on_focus_in_value);
+}
+
+
+} // namespace Widget
+} // 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/widget/spinbutton.h b/src/ui/widget/spinbutton.h
new file mode 100644
index 0000000..710b511
--- /dev/null
+++ b/src/ui/widget/spinbutton.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2011 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SPINBUTTON_H
+#define INKSCAPE_UI_WIDGET_SPINBUTTON_H
+
+#include <gtkmm/spinbutton.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class UnitMenu;
+class UnitTracker;
+
+/**
+ * SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMenu),
+ * and allows entry of both '.' and ',' for the decimal, even when in numeric mode.
+ *
+ * Calling "set_numeric()" effectively disables the expression parsing. If no unit menu is linked, all unitlike characters are ignored.
+ */
+class SpinButton : public Gtk::SpinButton
+{
+public:
+ SpinButton(double climb_rate = 0.0, guint digits = 0)
+ : Gtk::SpinButton(climb_rate, digits),
+ _unit_menu(nullptr),
+ _unit_tracker(nullptr),
+ _on_focus_in_value(0.)
+ {
+ connect_signals();
+ };
+ explicit SpinButton(Glib::RefPtr<Gtk::Adjustment>& adjustment, double climb_rate = 0.0, guint digits = 0)
+ : Gtk::SpinButton(adjustment, climb_rate, digits),
+ _unit_menu(nullptr),
+ _unit_tracker(nullptr),
+ _on_focus_in_value(0.)
+ {
+ connect_signals();
+ };
+
+ ~SpinButton() override = default;
+
+ // noncopyable
+ SpinButton(const SpinButton&) = delete;
+ SpinButton& operator=(const SpinButton&) = delete;
+
+ void setUnitMenu(UnitMenu* unit_menu) { _unit_menu = unit_menu; };
+
+ void addUnitTracker(UnitTracker* ut) { _unit_tracker = ut; };
+
+protected:
+ UnitMenu *_unit_menu; /// Linked unit menu for unit conversion in entered expressions.
+ UnitTracker *_unit_tracker; // Linked unit tracker for unit conversion in entered expressions.
+ double _on_focus_in_value;
+
+ void connect_signals();
+
+ /**
+ * This callback function should try to convert the entered text to a number and write it to newvalue.
+ * It calls a method to evaluate the (potential) mathematical expression.
+ *
+ * @retval false No conversion done, continue with default handler.
+ * @retval true Conversion successful, don't call default handler.
+ */
+ int on_input(double* newvalue) override;
+
+ /**
+ * When focus is obtained, save the value to enable undo later.
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_my_focus_in_event(GdkEventFocus* event);
+
+ /**
+ * When scroll is done.
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_scroll_event(GdkEventScroll *event) override;
+ /**
+ * Handle specific keypress events, like Ctrl+Z.
+ *
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_my_key_press_event(GdkEventKey* event);
+
+ /**
+ * Undo the editing, by resetting the value upon when the spinbutton got focus.
+ */
+ void undo();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPINBUTTON_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/widget/style-subject.cpp b/src/ui/widget/style-subject.cpp
new file mode 100644
index 0000000..9c30a42
--- /dev/null
+++ b/src/ui/widget/style-subject.cpp
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "style-subject.h"
+
+#include "desktop.h"
+#include "desktop-style.h"
+#include "selection.h"
+
+#include "xml/sp-css-attr.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+StyleSubject::StyleSubject() : _desktop(nullptr) {
+}
+
+StyleSubject::~StyleSubject() {
+ setDesktop(nullptr);
+}
+
+void StyleSubject::setDesktop(SPDesktop *desktop) {
+ if (desktop != _desktop) {
+ if (desktop) {
+ GC::anchor(desktop);
+ }
+ if (_desktop) {
+ GC::release(_desktop);
+ }
+ _desktop = desktop;
+ _afterDesktopSwitch(desktop);
+ _emitChanged();
+ }
+}
+
+StyleSubject::Selection::Selection() = default;
+
+StyleSubject::Selection::~Selection() = default;
+
+Inkscape::Selection *StyleSubject::Selection::_getSelection() const {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ return desktop->getSelection();
+ } else {
+ return nullptr;
+ }
+}
+
+std::vector<SPObject*> StyleSubject::Selection::list() {
+ Inkscape::Selection *selection = _getSelection();
+ if(selection) {
+ return std::vector<SPObject *>(selection->objects().begin(), selection->objects().end());
+ }
+
+ return std::vector<SPObject*>();
+}
+
+Geom::OptRect StyleSubject::Selection::getBounds(SPItem::BBoxType type) {
+ Inkscape::Selection *selection = _getSelection();
+ if (selection) {
+ return selection->bounds(type);
+ } else {
+ return Geom::OptRect();
+ }
+}
+
+int StyleSubject::Selection::queryStyle(SPStyle *query, int property) {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ return sp_desktop_query_style(desktop, query, property);
+ } else {
+ return QUERY_STYLE_NOTHING;
+ }
+}
+
+void StyleSubject::Selection::_afterDesktopSwitch(SPDesktop *desktop) {
+ _sel_changed.disconnect();
+ _subsel_changed.disconnect();
+ _sel_modified.disconnect();
+ if (desktop) {
+ _subsel_changed = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged)));
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (selection) {
+ _sel_changed = selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged)));
+ _sel_modified = selection->connectModified(sigc::mem_fun(*this, &Selection::_emitModified));
+ }
+ }
+}
+
+void StyleSubject::Selection::setCSS(SPCSSAttr *css) {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ sp_desktop_set_style(desktop, css);
+ }
+}
+
+StyleSubject::CurrentLayer::CurrentLayer() {
+ _element = nullptr;
+}
+
+StyleSubject::CurrentLayer::~CurrentLayer() = default;
+
+void StyleSubject::CurrentLayer::_setLayer(SPObject *layer) {
+ _layer_release.disconnect();
+ _layer_modified.disconnect();
+ if (_element) {
+ sp_object_unref(_element, nullptr);
+ }
+ _element = layer;
+ if (layer) {
+ sp_object_ref(layer, nullptr);
+ _layer_release = layer->connectRelease(sigc::hide(sigc::bind(sigc::mem_fun(*this, &CurrentLayer::_setLayer), (SPObject *)nullptr)));
+ _layer_modified = layer->connectModified(sigc::hide(sigc::hide(sigc::mem_fun(*this, &CurrentLayer::_emitChanged))));
+ }
+ _emitChanged();
+}
+
+SPObject *StyleSubject::CurrentLayer::_getLayer() const {
+ return _element;
+}
+
+SPObject *StyleSubject::CurrentLayer::_getLayerSList() const {
+ return _element;
+
+}
+
+std::vector<SPObject*> StyleSubject::CurrentLayer::list(){
+ std::vector<SPObject*> list;
+ list.push_back(_element);
+ return list;
+}
+
+Geom::OptRect StyleSubject::CurrentLayer::getBounds(SPItem::BBoxType type) {
+ SPObject *layer = _getLayer();
+ if (layer && SP_IS_ITEM(layer)) {
+ return SP_ITEM(layer)->desktopBounds(type);
+ } else {
+ return Geom::OptRect();
+ }
+}
+
+int StyleSubject::CurrentLayer::queryStyle(SPStyle *query, int property) {
+ std::vector<SPItem*> list;
+ SPObject* i=_getLayerSList();
+ if (i) {
+ list.push_back((SPItem*)i);
+ return sp_desktop_query_style_from_list(list, query, property);
+ } else {
+ return QUERY_STYLE_NOTHING;
+ }
+}
+
+void StyleSubject::CurrentLayer::setCSS(SPCSSAttr *css) {
+ SPObject *layer = _getLayer();
+ if (layer) {
+ sp_desktop_apply_css_recursive(layer, css, true);
+ }
+}
+
+void StyleSubject::CurrentLayer::_afterDesktopSwitch(SPDesktop *desktop) {
+ _layer_switched.disconnect();
+ if (desktop) {
+ _layer_switched = desktop->connectCurrentLayerChanged(sigc::mem_fun(*this, &CurrentLayer::_setLayer));
+ _setLayer(desktop->currentLayer());
+ } else {
+ _setLayer(nullptr);
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/widget/style-subject.h b/src/ui/widget/style-subject.h
new file mode 100644
index 0000000..c2f2b3f
--- /dev/null
+++ b/src/ui/widget/style-subject.h
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Abstraction for different style widget operands.
+ */
+/*
+ * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H
+#define SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H
+
+#include <boost/optional.hpp>
+#include <2geom/rect.h>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+
+#include "object/sp-item.h"
+#include "object/sp-tag.h"
+#include "object/sp-tag-use.h"
+#include "object/sp-tag-use-reference.h"
+
+class SPDesktop;
+class SPObject;
+class SPCSSAttr;
+class SPStyle;
+
+namespace Inkscape {
+class Selection;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class StyleSubject {
+public:
+ class Selection;
+ class CurrentLayer;
+
+
+ StyleSubject();
+ virtual ~StyleSubject();
+
+ void setDesktop(SPDesktop *desktop);
+ SPDesktop *getDesktop() const { return _desktop; }
+
+ virtual Geom::OptRect getBounds(SPItem::BBoxType type) = 0;
+ virtual int queryStyle(SPStyle *query, int property) = 0;
+ virtual void setCSS(SPCSSAttr *css) = 0;
+ virtual std::vector<SPObject*> list(){return std::vector<SPObject*>();};
+
+ sigc::connection connectChanged(sigc::signal<void>::slot_type slot) {
+ return _changed_signal.connect(slot);
+ }
+
+protected:
+ virtual void _afterDesktopSwitch(SPDesktop */*desktop*/) {}
+ void _emitChanged() { _changed_signal.emit(); }
+ void _emitModified(Inkscape::Selection* selection, guint flags) {
+ // Do not say this object has styles unless it's style has been modified
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ _emitChanged();
+ }
+ }
+
+private:
+ sigc::signal<void> _changed_signal;
+ SPDesktop *_desktop;
+};
+
+class StyleSubject::Selection : public StyleSubject {
+public:
+ Selection();
+ ~Selection() override;
+
+ Geom::OptRect getBounds(SPItem::BBoxType type) override;
+ int queryStyle(SPStyle *query, int property) override;
+ void setCSS(SPCSSAttr *css) override;
+ std::vector<SPObject*> list() override;
+
+protected:
+ void _afterDesktopSwitch(SPDesktop *desktop) override;
+
+private:
+ Inkscape::Selection *_getSelection() const;
+
+ sigc::connection _sel_changed;
+ sigc::connection _subsel_changed;
+ sigc::connection _sel_modified;
+};
+
+class StyleSubject::CurrentLayer : public StyleSubject {
+public:
+ CurrentLayer();
+ ~CurrentLayer() override;
+
+ Geom::OptRect getBounds(SPItem::BBoxType type) override;
+ int queryStyle(SPStyle *query, int property) override;
+ void setCSS(SPCSSAttr *css) override;
+ std::vector<SPObject*> list() override;
+
+protected:
+ void _afterDesktopSwitch(SPDesktop *desktop) override;
+
+private:
+ SPObject *_getLayer() const;
+ void _setLayer(SPObject *layer);
+ SPObject *_getLayerSList() const;
+
+ sigc::connection _layer_switched;
+ sigc::connection _layer_release;
+ sigc::connection _layer_modified;
+ mutable SPObject* _element;
+};
+
+}
+}
+}
+
+#endif // SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_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/widget/style-swatch.cpp b/src/ui/widget/style-swatch.cpp
new file mode 100644
index 0000000..734f092
--- /dev/null
+++ b/src/ui/widget/style-swatch.cpp
@@ -0,0 +1,373 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Static style swatch (fill, stroke, opacity).
+ */
+/* Authors:
+ * buliabyak@gmail.com
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "style-swatch.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "object/sp-linear-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "style.h"
+
+#include "helper/action.h"
+
+#include "ui/widget/color-preview.h"
+#include "util/units.h"
+
+#include "widgets/spw-utilities.h"
+#include "widgets/widget-sizes.h"
+
+#include "xml/sp-css-attr.h"
+
+enum {
+ SS_FILL,
+ SS_STROKE
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Watches whether the tool uses the current style.
+ */
+class StyleSwatch::ToolObserver : public Inkscape::Preferences::Observer {
+public:
+ ToolObserver(Glib::ustring const &path, StyleSwatch &ss) :
+ Observer(path),
+ _style_swatch(ss)
+ {}
+ void notify(Inkscape::Preferences::Entry const &val) override;
+private:
+ StyleSwatch &_style_swatch;
+};
+
+/**
+ * Watches for changes in the observed style pref.
+ */
+class StyleSwatch::StyleObserver : public Inkscape::Preferences::Observer {
+public:
+ StyleObserver(Glib::ustring const &path, StyleSwatch &ss) :
+ Observer(path),
+ _style_swatch(ss)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->notify(prefs->getEntry(path));
+ }
+ void notify(Inkscape::Preferences::Entry const &val) override {
+ SPCSSAttr *css = val.getInheritedStyle();
+ _style_swatch.setStyle(css);
+ sp_repr_css_attr_unref(css);
+ }
+private:
+ StyleSwatch &_style_swatch;
+};
+
+void StyleSwatch::ToolObserver::notify(Inkscape::Preferences::Entry const &val)
+{
+ bool usecurrent = val.getBool();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_style_swatch._style_obs) delete _style_swatch._style_obs;
+
+ if (usecurrent) {
+ _style_swatch._style_obs = new StyleObserver("/desktop/style", _style_swatch);
+
+ // If desktop's last-set style is empty, a tool uses its own fixed style even if set to use
+ // last-set (so long as it's empty). To correctly show this, we get the tool's style
+ // if the desktop's style is empty.
+ SPCSSAttr *css = prefs->getStyle("/desktop/style");
+ if (!css->attributeList()) {
+ SPCSSAttr *css2 = prefs->getInheritedStyle(_style_swatch._tool_path + "/style");
+ _style_swatch.setStyle(css2);
+ sp_repr_css_attr_unref(css2);
+ }
+ sp_repr_css_attr_unref(css);
+ } else {
+ _style_swatch._style_obs = new StyleObserver(_style_swatch._tool_path + "/style", _style_swatch);
+ }
+ prefs->addObserver(*_style_swatch._style_obs);
+}
+
+StyleSwatch::StyleSwatch(SPCSSAttr *css, gchar const *main_tip)
+ :
+ _desktop(nullptr),
+ _verb_t(0),
+ _css(nullptr),
+ _tool_obs(nullptr),
+ _style_obs(nullptr),
+ _table(Gtk::manage(new Gtk::Grid())),
+ _sw_unit(nullptr)
+{
+ set_name("StyleSwatch");
+
+ _label[SS_FILL].set_markup(_("Fill:"));
+ _label[SS_STROKE].set_markup(_("Stroke:"));
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ _label[i].set_halign(Gtk::ALIGN_START);
+ _label[i].set_valign(Gtk::ALIGN_CENTER);
+ _label[i].set_margin_top(0);
+ _label[i].set_margin_bottom(0);
+ _label[i].set_margin_start(0);
+ _label[i].set_margin_end(0);
+
+ _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0);
+ }
+
+ _opacity_value.set_halign(Gtk::ALIGN_START);
+ _opacity_value.set_valign(Gtk::ALIGN_CENTER);
+ _opacity_value.set_margin_top(0);
+ _opacity_value.set_margin_bottom(0);
+ _opacity_value.set_margin_start(0);
+ _opacity_value.set_margin_end(0);
+
+ _table->set_column_spacing(2);
+ _table->set_row_spacing(0);
+
+ _stroke.pack_start(_place[SS_STROKE]);
+ _stroke_width_place.add(_stroke_width);
+ _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK);
+
+ _opacity_place.add(_opacity_value);
+
+ _table->attach(_label[SS_FILL], 0, 0, 1, 1);
+ _table->attach(_label[SS_STROKE], 0, 1, 1, 1);
+ _table->attach(_place[SS_FILL], 1, 0, 1, 1);
+ _table->attach(_stroke, 1, 1, 1, 1);
+ _table->attach(_opacity_place, 2, 0, 1, 2);
+
+ _swatch.add(*_table);
+ pack_start(_swatch, true, true, 0);
+
+ set_size_request (STYLE_SWATCH_WIDTH, -1);
+
+ setStyle (css);
+
+ _swatch.signal_button_press_event().connect(sigc::mem_fun(*this, &StyleSwatch::on_click));
+
+ if (main_tip)
+ {
+ _swatch.set_tooltip_text(main_tip);
+ }
+}
+
+void StyleSwatch::setClickVerb(sp_verb_t verb_t) {
+ _verb_t = verb_t;
+}
+
+void StyleSwatch::setDesktop(SPDesktop *desktop) {
+ _desktop = desktop;
+}
+
+bool
+StyleSwatch::on_click(GdkEventButton */*event*/)
+{
+ if (this->_desktop && this->_verb_t != SP_VERB_NONE) {
+ Inkscape::Verb *verb = Inkscape::Verb::get(this->_verb_t);
+ SPAction *action = verb->get_action(Inkscape::ActionContext((Inkscape::UI::View::View *) this->_desktop));
+ sp_action_perform (action, nullptr);
+ return true;
+ }
+ return false;
+}
+
+StyleSwatch::~StyleSwatch()
+{
+ if (_css)
+ sp_repr_css_attr_unref (_css);
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ delete _color_preview[i];
+ }
+
+ if (_style_obs) delete _style_obs;
+ if (_tool_obs) delete _tool_obs;
+}
+
+void
+StyleSwatch::setWatchedTool(const char *path, bool synthesize)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (_tool_obs) {
+ delete _tool_obs;
+ _tool_obs = nullptr;
+ }
+
+ if (path) {
+ _tool_path = path;
+ _tool_obs = new ToolObserver(_tool_path + "/usecurrent", *this);
+ prefs->addObserver(*_tool_obs);
+ } else {
+ _tool_path = "";
+ }
+
+ // hack until there is a real synthesize events function for prefs,
+ // which shouldn't be hard to write once there is sufficient need for it
+ if (synthesize && _tool_obs) {
+ _tool_obs->notify(prefs->getEntry(_tool_path + "/usecurrent"));
+ }
+}
+
+
+void StyleSwatch::setStyle(SPCSSAttr *css)
+{
+ if (_css)
+ sp_repr_css_attr_unref (_css);
+
+ if (!css)
+ return;
+
+ _css = sp_repr_css_attr_new();
+ sp_repr_css_merge(_css, css);
+
+ Glib::ustring css_string;
+ sp_repr_css_write_string (_css, css_string);
+
+ SPStyle style(SP_ACTIVE_DOCUMENT);
+ if (!css_string.empty()) {
+ style.mergeString(css_string.c_str());
+ }
+ setStyle (&style);
+}
+
+void StyleSwatch::setStyle(SPStyle *query)
+{
+ _place[SS_FILL].remove();
+ _place[SS_STROKE].remove();
+
+ bool has_stroke = true;
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ Gtk::EventBox *place = &(_place[i]);
+
+ SPIPaint *paint;
+ if (i == SS_FILL) {
+ paint = &(query->fill);
+ } else {
+ paint = &(query->stroke);
+ }
+
+ if (paint->set && paint->isPaintserver()) {
+ SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (query) : SP_STYLE_STROKE_SERVER (query);
+
+ if (SP_IS_LINEARGRADIENT (server)) {
+ _value[i].set_markup(_("L Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Linear gradient fill")) : (_("Linear gradient stroke")));
+ } else if (SP_IS_RADIALGRADIENT (server)) {
+ _value[i].set_markup(_("R Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Radial gradient fill")) : (_("Radial gradient stroke")));
+ } else if (SP_IS_PATTERN (server)) {
+ _value[i].set_markup(_("Pattern"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Pattern fill")) : (_("Pattern stroke")));
+ }
+
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) );
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar *tip;
+ if (i == SS_FILL) {
+ tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ } else {
+ tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ }
+ place->set_tooltip_text(tip);
+ g_free (tip);
+ } else if (paint->set && paint->isNone()) {
+ _value[i].set_markup(C_("Fill and stroke", "<i>None</i>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (C_("Fill and stroke", "No fill")) : (C_("Fill and stroke", "No stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ } else if (!paint->set) {
+ _value[i].set_markup(_("<b>Unset</b>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ }
+ }
+
+// Now query stroke_width
+ if (has_stroke) {
+ double w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(query->stroke_width.computed, "px", _sw_unit);
+ } else {
+ w = query->stroke_width.computed;
+ }
+
+ {
+ gchar *str = g_strdup_printf(" %.3g", w);
+ _stroke_width.set_markup(str);
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Stroke width: %.5g%s"),
+ w,
+ _sw_unit? _sw_unit->abbr.c_str() : "px");
+ _stroke_width_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ } else {
+ _stroke_width_place.set_tooltip_text("");
+ _stroke_width.set_markup("");
+ _stroke_width.set_has_tooltip(false);
+ }
+
+ gdouble op = SP_SCALE24_TO_FLOAT(query->opacity.value);
+ if (op != 1) {
+ {
+ gchar *str;
+ str = g_strdup_printf(_("O: %2.0f"), (op*100.0));
+ _opacity_value.set_markup (str);
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Opacity: %2.1f %%"), (op*100.0));
+ _opacity_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ } else {
+ _opacity_place.set_tooltip_text("");
+ _opacity_value.set_markup("");
+ _opacity_value.set_has_tooltip(false);
+ }
+
+ show_all();
+}
+
+} // namespace Widget
+} // 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/widget/style-swatch.h b/src/ui/widget/style-swatch.h
new file mode 100644
index 0000000..4c7dc51
--- /dev/null
+++ b/src/ui/widget/style-swatch.h
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Static style swatch (fill, stroke, opacity)
+ */
+/* Authors:
+ * buliabyak@gmail.com
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_CURRENT_STYLE_H
+#define INKSCAPE_UI_CURRENT_STYLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/enums.h>
+
+#include "desktop.h"
+#include "preferences.h"
+
+class SPStyle;
+class SPCSSAttr;
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+
+namespace Util {
+ class Unit;
+}
+
+namespace UI {
+namespace Widget {
+
+class StyleSwatch : public Gtk::HBox
+{
+public:
+ StyleSwatch (SPCSSAttr *attr, gchar const *main_tip);
+
+ ~StyleSwatch() override;
+
+ void setStyle(SPStyle *style);
+ void setStyle(SPCSSAttr *attr);
+ SPCSSAttr *getStyle();
+
+ void setWatchedTool (const char *path, bool synthesize);
+
+ void setClickVerb(sp_verb_t verb_t);
+ void setDesktop(SPDesktop *desktop);
+ bool on_click(GdkEventButton *event);
+
+private:
+ class ToolObserver;
+ class StyleObserver;
+
+ SPDesktop *_desktop;
+ sp_verb_t _verb_t;
+ SPCSSAttr *_css;
+ ToolObserver *_tool_obs;
+ StyleObserver *_style_obs;
+ Glib::ustring _tool_path;
+
+ Gtk::EventBox _swatch;
+
+ Gtk::Grid *_table;
+
+ Gtk::Label _label[2];
+ Gtk::EventBox _place[2];
+ Gtk::EventBox _opacity_place;
+ Gtk::Label _value[2];
+ Gtk::Label _opacity_value;
+ Gtk::Widget *_color_preview[2];
+ Glib::ustring __color[2];
+ Gtk::HBox _stroke;
+ Gtk::EventBox _stroke_width_place;
+ Gtk::Label _stroke_width;
+
+ Inkscape::Util::Unit *_sw_unit;
+
+friend class ToolObserver;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_BUTTON_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/widget/text.cpp b/src/ui/widget/text.cpp
new file mode 100644
index 0000000..656ec45
--- /dev/null
+++ b/src/ui/widget/text.cpp
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "text.h"
+#include <gtkmm/entry.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Text::Text(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Glib::ustring const Text::getText() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::Entry*>(_widget)->get_text();
+}
+
+void Text::setText(Glib::ustring const text)
+{
+ g_assert(_widget != nullptr);
+ setProgrammatically = true; // callback is supposed to reset back, if it cares
+ static_cast<Gtk::Entry*>(_widget)->set_text(text); // FIXME: set correctly
+}
+
+Glib::SignalProxy0<void> Text::signal_activate()
+{
+ return static_cast<Gtk::Entry*>(_widget)->signal_activate();
+}
+
+
+} // namespace Widget
+} // 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/widget/text.h b/src/ui/widget/text.h
new file mode 100644
index 0000000..87c9357
--- /dev/null
+++ b/src/ui/widget/text.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_TEXT_H
+#define INKSCAPE_UI_WIDGET_TEXT_H
+
+#include "labelled.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with optional icon or suffix, for entering arbitrary number values.
+ */
+class Text : public Labelled
+{
+public:
+
+ /**
+ * Construct a Text Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Text(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Get the text in the entry.
+ */
+ Glib::ustring const getText() const;
+
+ /**
+ * Sets the text of the text entry.
+ */
+ void setText(Glib::ustring const text);
+
+ void update();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_activate();
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_TEXT_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/widget/tolerance-slider.cpp b/src/ui/widget/tolerance-slider.cpp
new file mode 100644
index 0000000..b1b28a7
--- /dev/null
+++ b/src/ui/widget/tolerance-slider.cpp
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tolerance-slider.h"
+
+#include "registry.h"
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scale.h>
+
+#include "inkscape.h"
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+
+#include "object/sp-namedview.h"
+
+#include "svg/stringstream.h"
+
+#include "xml/repr.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+
+
+//====================================================
+
+ToleranceSlider::ToleranceSlider(const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr)
+: _vbox(nullptr)
+{
+ init(label1, label2, label3, tip1, tip2, tip3, key, wr);
+}
+
+ToleranceSlider::~ToleranceSlider()
+{
+ if (_vbox) delete _vbox;
+ _scale_changed_connection.disconnect();
+}
+
+void ToleranceSlider::init (const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr)
+{
+ // hbox = label + slider
+ //
+ // e.g.
+ //
+ // snap distance |-------X---| 37
+
+ // vbox = checkbutton
+ // +
+ // hbox
+
+ _vbox = new Gtk::VBox;
+ _hbox = Gtk::manage(new Gtk::HBox);
+
+ Gtk::Label *theLabel1 = Gtk::manage(new Gtk::Label(label1));
+ theLabel1->set_use_underline();
+ theLabel1->set_halign(Gtk::ALIGN_START);
+ theLabel1->set_valign(Gtk::ALIGN_CENTER);
+ // align the label with the checkbox text above by indenting 22 px.
+ _hbox->pack_start(*theLabel1, Gtk::PACK_EXPAND_WIDGET, 22);
+
+ _hscale = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+ _hscale->set_range(1.0, 51.0);
+
+ theLabel1->set_mnemonic_widget (*_hscale);
+ _hscale->set_draw_value (true);
+ _hscale->set_value_pos (Gtk::POS_RIGHT);
+ _hscale->set_size_request (100, -1);
+ _old_val = 10;
+ _hscale->set_value (_old_val);
+ _hscale->set_tooltip_text (tip1);
+ _hbox->add (*_hscale);
+
+
+ Gtk::Label *theLabel2 = Gtk::manage(new Gtk::Label(label2));
+ theLabel2->set_use_underline();
+ Gtk::Label *theLabel3 = Gtk::manage(new Gtk::Label(label3));
+ theLabel3->set_use_underline();
+ _button1 = Gtk::manage(new Gtk::RadioButton);
+ _radio_button_group = _button1->get_group();
+ _button2 = Gtk::manage(new Gtk::RadioButton);
+ _button2->set_group(_radio_button_group);
+ _button1->set_tooltip_text (tip2);
+ _button2->set_tooltip_text (tip3);
+ _button1->add (*theLabel3);
+ _button1->set_halign(Gtk::ALIGN_START);
+ _button1->set_valign(Gtk::ALIGN_CENTER);
+ _button2->add (*theLabel2);
+ _button2->set_halign(Gtk::ALIGN_START);
+ _button2->set_valign(Gtk::ALIGN_CENTER);
+
+ _vbox->add (*_button1);
+ _vbox->add (*_button2);
+ // Here we need some extra pixels to get the vertical spacing right. Why?
+ _vbox->pack_end(*_hbox, true, true, 3); // add 3 px.
+ _key = key;
+ _scale_changed_connection = _hscale->signal_value_changed().connect (sigc::mem_fun (*this, &ToleranceSlider::on_scale_changed));
+ _btn_toggled_connection = _button2->signal_toggled().connect (sigc::mem_fun (*this, &ToleranceSlider::on_toggled));
+ _wr = &wr;
+ _vbox->show_all_children();
+}
+
+void ToleranceSlider::setValue (double val)
+{
+ auto adj = _hscale->get_adjustment();
+
+ adj->set_lower (1.0);
+ adj->set_upper (51.0);
+ adj->set_step_increment (1.0);
+
+ if (val > 9999.9) // magic value 10000.0
+ {
+ _button1->set_active (true);
+ _button2->set_active (false);
+ _hbox->set_sensitive (false);
+ val = 50.0;
+ }
+ else
+ {
+ _button1->set_active (false);
+ _button2->set_active (true);
+ _hbox->set_sensitive (true);
+ }
+ _hscale->set_value (val);
+ _hbox->show_all();
+}
+
+void ToleranceSlider::setLimits (double theMin, double theMax)
+{
+ _hscale->set_range (theMin, theMax);
+ _hscale->get_adjustment()->set_step_increment (1);
+}
+
+void ToleranceSlider::on_scale_changed()
+{
+ update (_hscale->get_value());
+}
+
+void ToleranceSlider::on_toggled()
+{
+ if (!_button2->get_active())
+ {
+ _old_val = _hscale->get_value();
+ _hbox->set_sensitive (false);
+ _hbox->show_all();
+ setValue (10000.0);
+ update (10000.0);
+ }
+ else
+ {
+ _hbox->set_sensitive (true);
+ _hbox->show_all();
+ setValue (_old_val);
+ update (_old_val);
+ }
+}
+
+void ToleranceSlider::update (double val)
+{
+ if (_wr->isUpdating())
+ return;
+
+ SPDesktop *dt = SP_ACTIVE_DESKTOP;
+ if (!dt)
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << val;
+
+ _wr->setUpdating (true);
+
+ SPDocument *doc = dt->getDocument();
+ bool saved = DocumentUndo::getUndoSensitive(doc);
+ DocumentUndo::setUndoSensitive(doc, false);
+ Inkscape::XML::Node *repr = dt->getNamedView()->getRepr();
+ repr->setAttribute(_key, os.str());
+ DocumentUndo::setUndoSensitive(doc, saved);
+
+ doc->setModifiedSinceSave();
+
+ _wr->setUpdating (false);
+}
+
+
+} // 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/widget/tolerance-slider.h b/src/ui/widget/tolerance-slider.h
new file mode 100644
index 0000000..1c4af1d
--- /dev/null
+++ b/src/ui/widget/tolerance-slider.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_
+#define INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_
+
+#include <gtkmm/radiobuttongroup.h>
+
+namespace Gtk {
+class RadioButton;
+class Scale;
+class VBox;
+class HBox;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+/**
+ * Implementation of tolerance slider widget.
+ * This widget is part of the Document properties dialog.
+ */
+class ToleranceSlider {
+public:
+ ToleranceSlider(const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& label3,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& tip3,
+ const Glib::ustring& key,
+ Registry& wr);
+ ~ToleranceSlider();
+ void setValue (double);
+ void setLimits (double, double);
+ Gtk::VBox* _vbox;
+private:
+ void init (const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& label3,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& tip3,
+ const Glib::ustring& key,
+ Registry& wr);
+
+protected:
+ void on_scale_changed();
+ void on_toggled();
+ void update (double val);
+ Gtk::HBox *_hbox;
+ Gtk::Scale *_hscale;
+ Gtk::RadioButtonGroup _radio_button_group;
+ Gtk::RadioButton *_button1;
+ Gtk::RadioButton *_button2;
+ Registry *_wr;
+ Glib::ustring _key;
+ sigc::connection _scale_changed_connection;
+ sigc::connection _btn_toggled_connection;
+ double _old_val;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__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/widget/unit-menu.cpp b/src/ui/widget/unit-menu.cpp
new file mode 100644
index 0000000..aaf565f
--- /dev/null
+++ b/src/ui/widget/unit-menu.cpp
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+
+#include "unit-menu.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+UnitMenu::UnitMenu() : _type(UNIT_TYPE_NONE)
+{
+ set_active(0);
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &UnitMenu::on_scroll_event));
+}
+
+UnitMenu::~UnitMenu() = default;
+
+bool UnitMenu::setUnitType(UnitType unit_type)
+{
+ // Expand the unit widget with unit entries from the unit table
+ UnitTable::UnitMap m = unit_table.units(unit_type);
+
+ for (auto & i : m) {
+ append(i.first);
+ }
+ _type = unit_type;
+ set_active_text(unit_table.primary(unit_type));
+
+ return true;
+}
+
+bool UnitMenu::resetUnitType(UnitType unit_type)
+{
+ remove_all();
+
+ return setUnitType(unit_type);
+}
+
+void UnitMenu::addUnit(Unit const& u)
+{
+ unit_table.addUnit(u, false);
+ append(u.abbr);
+}
+
+Unit const * UnitMenu::getUnit() const
+{
+ if (get_active_text() == "") {
+ g_assert(_type != UNIT_TYPE_NONE);
+ return unit_table.getUnit(unit_table.primary(_type));
+ }
+ return unit_table.getUnit(get_active_text());
+}
+
+bool UnitMenu::setUnit(Glib::ustring const & unit)
+{
+ // TODO: Determine if 'unit' is available in the dropdown.
+ // If not, return false
+
+ set_active_text(unit);
+ return true;
+}
+
+Glib::ustring UnitMenu::getUnitAbbr() const
+{
+ if (get_active_text() == "") {
+ return "";
+ }
+ return getUnit()->abbr;
+}
+
+UnitType UnitMenu::getUnitType() const
+{
+ return getUnit()->type;
+}
+
+double UnitMenu::getUnitFactor() const
+{
+ return getUnit()->factor;
+}
+
+int UnitMenu::getDefaultDigits() const
+{
+ return getUnit()->defaultDigits();
+}
+
+double UnitMenu::getDefaultStep() const
+{
+ int factor_digits = -1*int(log10(getUnit()->factor));
+ return pow(10.0, factor_digits);
+}
+
+double UnitMenu::getDefaultPage() const
+{
+ return 10 * getDefaultStep();
+}
+
+double UnitMenu::getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr) const
+{
+ double old_factor = getUnit()->factor;
+ if (old_unit_abbr != "no_unit") {
+ old_factor = unit_table.getUnit(old_unit_abbr)->factor;
+ }
+ Unit const * new_unit = unit_table.getUnit(new_unit_abbr);
+
+ // Catch the case of zero or negative unit factors (error!)
+ if (old_factor < 0.0000001 ||
+ new_unit->factor < 0.0000001) {
+ // TODO: Should we assert here?
+ return 0.00;
+ }
+
+ return old_factor / new_unit->factor;
+}
+
+bool UnitMenu::isAbsolute() const
+{
+ return getUnitType() != UNIT_TYPE_DIMENSIONLESS;
+}
+
+bool UnitMenu::isRadial() const
+{
+ return getUnitType() == UNIT_TYPE_RADIAL;
+}
+
+bool UnitMenu::on_scroll_event(GdkEventScroll *event) { return false; }
+
+} // namespace Widget
+} // 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/widget/unit-menu.h b/src/ui/widget/unit-menu.h
new file mode 100644
index 0000000..b8e3ab7
--- /dev/null
+++ b/src/ui/widget/unit-menu.h
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_UNIT_H
+#define INKSCAPE_UI_WIDGET_UNIT_H
+
+#include <gtkmm/comboboxtext.h>
+#include "util/units.h"
+
+using namespace Inkscape::Util;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A drop down menu for choosing unit types.
+ */
+class UnitMenu : public Gtk::ComboBoxText
+{
+public:
+
+ /**
+ * Construct a UnitMenu
+ */
+ UnitMenu();
+
+ ~UnitMenu() override;
+
+ /**
+ * Adds the unit type to the widget. This extracts the corresponding
+ * units from the unit map matching the given type, and appends them
+ * to the dropdown widget. It causes the primary unit for the given
+ * unit_type to be selected.
+ */
+ bool setUnitType(UnitType unit_type);
+
+ /**
+ * Removes all unit entries, then adds the unit type to the widget.
+ * This extracts the corresponding
+ * units from the unit map matching the given type, and appends them
+ * to the dropdown widget. It causes the primary unit for the given
+ * unit_type to be selected.
+ */
+ bool resetUnitType(UnitType unit_type);
+
+ /**
+ * Adds a unit, possibly user-defined, to the menu.
+ */
+ void addUnit(Unit const& u);
+
+ /**
+ * Sets the dropdown widget to the given unit abbreviation.
+ * Returns true if the unit was selectable, false if not
+ * (i.e., if the unit was not present in the widget).
+ */
+ bool setUnit(Glib::ustring const &unit);
+
+ /**
+ * Returns the Unit object corresponding to the current selection
+ * in the dropdown widget.
+ */
+ Unit const * getUnit() const;
+
+ /**
+ * Returns the abbreviated unit name of the selected unit.
+ */
+ Glib::ustring getUnitAbbr() const;
+
+ /**
+ * Returns the UnitType of the selected unit.
+ */
+ UnitType getUnitType() const;
+
+ /**
+ * Returns the unit factor for the selected unit.
+ */
+ double getUnitFactor() const;
+
+ /**
+ * Returns the recommended number of digits for displaying
+ * numbers of this unit type.
+ */
+ int getDefaultDigits() const;
+
+ /**
+ * Returns the recommended step size in spin buttons
+ * displaying units of this type.
+ */
+ double getDefaultStep() const;
+
+ /**
+ * Returns the recommended page size (when hitting pgup/pgdn)
+ * in spin buttons displaying units of this type.
+ */
+ double getDefaultPage() const;
+
+ /**
+ * Returns the conversion factor required to convert values
+ * of the currently selected unit into units of type
+ * new_unit_abbr.
+ */
+ double getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr = "no_unit") const;
+
+ /**
+ * Returns true if the selected unit is not dimensionless
+ * (false for %, true for px, pt, cm, etc).
+ */
+ bool isAbsolute() const;
+
+ /**
+ * Returns true if the selected unit is radial (deg or rad).
+ */
+ bool isRadial() const;
+
+protected:
+ UnitType _type;
+ /**
+ * block scroll from widget if is inside a scrolled window.
+ */
+ bool on_scroll_event(GdkEventScroll *event) override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_UNIT_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/widget/unit-tracker.cpp b/src/ui/widget/unit-tracker.cpp
new file mode 100644
index 0000000..40d5ccf
--- /dev/null
+++ b/src/ui/widget/unit-tracker.cpp
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::UnitTracker
+ * Simple mediator to synchronize changes to unit menus
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ * Matthew Petroff <matthew@mpetroff.net>
+ *
+ * Copyright (C) 2007 Jon A. Cruz
+ * Copyright (C) 2013 Matthew Petroff
+ * Copyright (C) 2018 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include <iostream>
+
+#include "unit-tracker.h"
+
+#include "combo-tool-item.h"
+
+#define COLUMN_STRING 0
+
+using Inkscape::Util::UnitTable;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+UnitTracker::UnitTracker(UnitType unit_type) :
+ _active(0),
+ _isUpdating(false),
+ _activeUnit(nullptr),
+ _activeUnitInitialized(false),
+ _store(nullptr),
+ _priorValues()
+{
+ UnitTable::UnitMap m = unit_table.units(unit_type);
+
+ ComboToolItemColumns columns;
+ _store = Gtk::ListStore::create(columns);
+ Gtk::TreeModel::Row row;
+
+ for (auto & m_iter : m) {
+
+ Glib::ustring unit = m_iter.first;
+
+ row = *(_store->append());
+ row[columns.col_label ] = unit;
+ row[columns.col_value ] = unit;
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+ }
+
+ // Why?
+ gint count = _store->children().size();
+ if ((count > 0) && (_active > count)) {
+ _setActive(--count);
+ } else {
+ _setActive(_active);
+ }
+}
+
+UnitTracker::~UnitTracker()
+{
+ _combo_list.clear();
+
+ // Unhook weak references to GtkAdjustments
+ for (auto i : _adjList) {
+ g_object_weak_unref(G_OBJECT(i), _adjustmentFinalizedCB, this);
+ }
+ _adjList.clear();
+}
+
+bool UnitTracker::isUpdating() const
+{
+ return _isUpdating;
+}
+
+Inkscape::Util::Unit const * UnitTracker::getActiveUnit() const
+{
+ return _activeUnit;
+}
+
+void UnitTracker::changeLabel(Glib::ustring new_label, gint pos, bool onlylabel)
+{
+ ComboToolItemColumns columns;
+ _store->children()[pos][columns.col_label] = new_label;
+ if (!onlylabel) {
+ _store->children()[pos][columns.col_value] = new_label;
+ }
+}
+
+void UnitTracker::setActiveUnit(Inkscape::Util::Unit const *unit)
+{
+ if (unit) {
+
+ ComboToolItemColumns columns;
+ int index = 0;
+ for (auto& row: _store->children() ) {
+ Glib::ustring storedUnit = row[columns.col_value];
+ if (!unit->abbr.compare (storedUnit)) {
+ _setActive (index);
+ break;
+ }
+ index++;
+ }
+ }
+}
+
+void UnitTracker::setActiveUnitByAbbr(gchar const *abbr)
+{
+ Inkscape::Util::Unit const *u = unit_table.getUnit(abbr);
+ setActiveUnit(u);
+}
+
+void UnitTracker::addAdjustment(GtkAdjustment *adj)
+{
+ if (std::find(_adjList.begin(),_adjList.end(),adj) == _adjList.end()) {
+ g_object_weak_ref(G_OBJECT(adj), _adjustmentFinalizedCB, this);
+ _adjList.push_back(adj);
+ } else {
+ std::cerr << "UnitTracker::addAjustment: Adjustment already added!" << std::endl;
+ }
+}
+
+void UnitTracker::addUnit(Inkscape::Util::Unit const *u)
+{
+ ComboToolItemColumns columns;
+
+ Gtk::TreeModel::Row row;
+ row = *(_store->append());
+ row[columns.col_label ] = u ? u->abbr.c_str() : "";
+ row[columns.col_value ] = u ? u->abbr.c_str() : "";
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+}
+
+void UnitTracker::prependUnit(Inkscape::Util::Unit const *u)
+{
+ ComboToolItemColumns columns;
+
+ Gtk::TreeModel::Row row;
+ row = *(_store->prepend());
+ row[columns.col_label ] = u ? u->abbr.c_str() : "";
+ row[columns.col_value ] = u ? u->abbr.c_str() : "";
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ /* Re-shuffle our default selection here (_active gets out of sync) */
+ setActiveUnit(_activeUnit);
+
+}
+
+void UnitTracker::setFullVal(GtkAdjustment *adj, gdouble val)
+{
+ _priorValues[adj] = val;
+}
+
+ComboToolItem *
+UnitTracker::create_tool_item(Glib::ustring const &label,
+ Glib::ustring const &tooltip)
+{
+ auto combo = ComboToolItem::create(label, tooltip, "NotUsed", _store);
+ combo->set_active(_active);
+ combo->signal_changed().connect(sigc::mem_fun(*this, &UnitTracker::_unitChangedCB));
+ combo->set_data("unit-tracker", this);
+ _combo_list.push_back(combo);
+ return combo;
+}
+
+void UnitTracker::_unitChangedCB(int active)
+{
+ _setActive(active);
+}
+
+void UnitTracker::_adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was)
+{
+ if (data && where_the_object_was) {
+ UnitTracker *self = reinterpret_cast<UnitTracker *>(data);
+ self->_adjustmentFinalized(where_the_object_was);
+ }
+}
+
+void UnitTracker::_adjustmentFinalized(GObject *where_the_object_was)
+{
+ GtkAdjustment* adj = (GtkAdjustment*)(where_the_object_was);
+ auto it = std::find(_adjList.begin(),_adjList.end(), adj);
+ if (it != _adjList.end()) {
+ _adjList.erase(it);
+ } else {
+ g_warning("Received a finalization callback for unknown object %p", where_the_object_was);
+ }
+}
+
+void UnitTracker::_setActive(gint active)
+{
+ if ( active != _active || !_activeUnitInitialized ) {
+ gint oldActive = _active;
+
+ if (_store) {
+
+ // Find old and new units
+ ComboToolItemColumns columns;
+ int index = 0;
+ Glib::ustring oldAbbr( "NotFound" );
+ Glib::ustring newAbbr( "NotFound" );
+ for (auto& row: _store->children() ) {
+ if (index == _active) {
+ oldAbbr = row[columns.col_value];
+ }
+ if (index == active) {
+ newAbbr = row[columns.col_value];
+ }
+ if (newAbbr != "NotFound" && oldAbbr != "NotFound") break;
+ ++index;
+ }
+
+ if (oldAbbr != "NotFound") {
+
+ if (newAbbr != "NotFound") {
+ Inkscape::Util::Unit const *oldUnit = unit_table.getUnit(oldAbbr);
+ Inkscape::Util::Unit const *newUnit = unit_table.getUnit(newAbbr);
+ _activeUnit = newUnit;
+
+ if (!_adjList.empty()) {
+ _fixupAdjustments(oldUnit, newUnit);
+ }
+ } else {
+ std::cerr << "UnitTracker::_setActive: Did not find new unit: " << active << std::endl;
+ }
+
+ } else {
+ std::cerr << "UnitTracker::_setActive: Did not find old unit: " << oldActive
+ << " new: " << active << std::endl;
+ }
+ }
+ _active = active;
+
+ for (auto combo : _combo_list) {
+ if(combo) combo->set_active(active);
+ }
+
+ _activeUnitInitialized = true;
+ }
+}
+
+void UnitTracker::_fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit)
+{
+ _isUpdating = true;
+ for ( auto adj : _adjList ) {
+ gdouble oldVal = gtk_adjustment_get_value(adj);
+ gdouble val = oldVal;
+
+ if ( (oldUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS)
+ && (newUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) )
+ {
+ val = newUnit->factor * 100;
+ _priorValues[adj] = Inkscape::Util::Quantity::convert(oldVal, oldUnit, "px");
+ } else if ( (oldUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS)
+ && (newUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) )
+ {
+ if (_priorValues.find(adj) != _priorValues.end()) {
+ val = Inkscape::Util::Quantity::convert(_priorValues[adj], "px", newUnit);
+ }
+ } else {
+ val = Inkscape::Util::Quantity::convert(oldVal, oldUnit, newUnit);
+ }
+
+ gtk_adjustment_set_value(adj, val);
+ }
+ _isUpdating = false;
+}
+
+} // namespace Widget
+} // 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 :
diff --git a/src/ui/widget/unit-tracker.h b/src/ui/widget/unit-tracker.h
new file mode 100644
index 0000000..b85da06
--- /dev/null
+++ b/src/ui/widget/unit-tracker.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::UnitTracker
+ * Simple mediator to synchronize changes to unit menus
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ * Matthew Petroff <matthew@mpetroff.net>
+ *
+ * Copyright (C) 2007 Jon A. Cruz
+ * Copyright (C) 2013 Matthew Petroff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_UNIT_TRACKER_H
+#define INKSCAPE_UI_WIDGET_UNIT_TRACKER_H
+
+#include <map>
+#include <vector>
+
+#include <gtkmm/liststore.h>
+
+#include "util/units.h"
+
+using Inkscape::Util::Unit;
+using Inkscape::Util::UnitType;
+
+typedef struct _GObject GObject;
+typedef struct _GtkAdjustment GtkAdjustment;
+typedef struct _GtkListStore GtkListStore;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class ComboToolItem;
+
+class UnitTracker {
+public:
+ UnitTracker(UnitType unit_type);
+ virtual ~UnitTracker();
+
+ bool isUpdating() const;
+
+ void setActiveUnit(Inkscape::Util::Unit const *unit);
+ void setActiveUnitByAbbr(gchar const *abbr);
+ Inkscape::Util::Unit const * getActiveUnit() const;
+
+ void addUnit(Inkscape::Util::Unit const *u);
+ void addAdjustment(GtkAdjustment *adj);
+ void prependUnit(Inkscape::Util::Unit const *u);
+ void setFullVal(GtkAdjustment *adj, gdouble val);
+ void changeLabel(Glib::ustring new_label, gint pos, bool onlylabel = false);
+
+ ComboToolItem *create_tool_item(Glib::ustring const &label,
+ Glib::ustring const &tooltip);
+
+protected:
+ UnitType _type;
+
+private:
+ // Callbacks
+ void _unitChangedCB(int active);
+ static void _adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was);
+
+ void _setActive(gint index);
+ void _fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit);
+
+ // Cleanup
+ void _adjustmentFinalized(GObject *where_the_object_was);
+
+ gint _active;
+ bool _isUpdating;
+ Inkscape::Util::Unit const *_activeUnit;
+ bool _activeUnitInitialized;
+
+ Glib::RefPtr<Gtk::ListStore> _store;
+ std::vector<ComboToolItem *> _combo_list;
+ std::vector<GtkAdjustment*> _adjList;
+ std::map <GtkAdjustment *, gdouble> _priorValues;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_UNIT_TRACKER_H